@commandgarden/cli 2.4.0 → 2.5.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.
@@ -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();
@@ -375,65 +392,154 @@ async function captureNewSchedule(knownIds) {
375
392
  return { newId, schedule, sawView, errMsg, items: [...itemsById.values()] };
376
393
  }
377
394
 
378
- // ── Sequential room processing ───────────────────────────────────────
395
+ /**
396
+ * Batch capture: wait for getSchedule responses covering all expected emails.
397
+ * Returns a Map of email (lowercased) -> { schedule, items, sawView, errMsg }.
398
+ */
399
+ async function captureBatchSchedules(expectedEmails, knownIds) {
400
+ const byEmail = new Map();
401
+
402
+ for (let i = 0; i < 80; i++) {
403
+ await sleep(100);
404
+ for (const pair of readCapture()) {
405
+ for (const [id, s] of schedulesFromPair(pair)) {
406
+ if (knownIds.has(id)) continue;
407
+ const idLower = id.toLowerCase();
408
+ if (!expectedEmails.has(idLower)) continue;
409
+ const items = (s.scheduleItems || []).filter(Boolean);
410
+ byEmail.set(idLower, {
411
+ schedule: s,
412
+ items,
413
+ sawView: !!(s.availabilityView && s.availabilityView.length),
414
+ errMsg: s.error ? (s.error.message || s.error.responseCode || 'unknown') : null,
415
+ });
416
+ }
417
+ }
418
+ if (expectedEmails.size > 0 && [...expectedEmails].every(e => byEmail.has(e))) break;
419
+ }
420
+
421
+ return byEmail;
422
+ }
423
+
424
+ // ── Room processing ──────────────────────────────────────────────────
379
425
 
380
426
  const allRows = [];
381
427
  const knownIds = new Set(seen);
382
428
 
383
- for (const room of rooms) {
384
- const isEmail = EMAIL_RE.test(room);
429
+ if (canBatch) {
430
+ // ── Batch mode: add all rooms via room finder, single capture ──────
431
+ const emailToName = new Map();
432
+ const expectedEmails = new Set();
433
+ for (const r of parsedRooms) {
434
+ emailToName.set(r.email, r.name);
435
+ expectedEmails.add(r.email);
436
+ }
385
437
 
386
- // Clear any pending captures before adding this room.
438
+ // Clear stale captures, then add all rooms without dismissing.
387
439
  readCapture();
388
440
 
389
- try {
390
- if (isEmail) {
391
- await addRoomByEmail(room);
392
- } else {
393
- await addRoomByName(room);
441
+ for (const r of parsedRooms) {
442
+ try {
443
+ await addRoomByName(r.name);
444
+ await sleep(300);
445
+ } catch (addErr) {
446
+ allRows.push({
447
+ roomName: r.name, roomEmail: r.email, date,
448
+ state: 'error', start: addErr.message, end: '', durationMin: 0,
449
+ });
450
+ expectedEmails.delete(r.email);
394
451
  }
395
- } catch (addErr) {
396
- allRows.push({
397
- roomName: room, roomEmail: '', date,
398
- state: 'error', start: addErr.message, end: '', durationMin: 0,
399
- });
400
- continue;
401
452
  }
402
453
 
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();
454
+ // Single batch capture for all successfully added rooms.
455
+ if (expectedEmails.size > 0) {
456
+ const results = await captureBatchSchedules(expectedEmails, knownIds);
457
+
458
+ for (const [email, data] of results) {
459
+ const name = emailToName.get(email);
460
+ knownIds.add(email);
461
+ if (data.errMsg && !data.sawView) {
462
+ allRows.push({
463
+ roomName: name, roomEmail: email, date,
464
+ state: 'error', start: `Free/busy error: ${data.errMsg}`,
465
+ end: '', durationMin: 0,
466
+ });
467
+ } else {
468
+ const timeline = buildTimeline(data.items, date);
469
+ for (const row of timeline) {
470
+ allRows.push({ roomName: name, roomEmail: email, ...row });
471
+ }
472
+ }
473
+ }
408
474
 
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;
475
+ // Report rooms not found in any captured response.
476
+ for (const email of expectedEmails) {
477
+ if (!results.has(email)) {
478
+ allRows.push({
479
+ roomName: emailToName.get(email), roomEmail: email, date,
480
+ state: 'error', start: `No free/busy returned for "${emailToName.get(email)}" on ${date}`,
481
+ end: '', durationMin: 0,
482
+ });
483
+ }
484
+ }
416
485
  }
486
+ } else {
487
+ // ── Sequential mode (original behavior) ────────────────────────────
488
+ for (const r of parsedRooms) {
489
+ const isEmail = r.mode === 'email';
490
+
491
+ // Clear any pending captures before adding this room.
492
+ readCapture();
493
+
494
+ try {
495
+ if (isEmail) {
496
+ await addRoomByEmail(r.name);
497
+ } else {
498
+ await addRoomByName(r.name);
499
+ }
500
+ } catch (addErr) {
501
+ allRows.push({
502
+ roomName: r.name, roomEmail: r.email || '', date,
503
+ state: 'error', start: addErr.message, end: '', durationMin: 0,
504
+ });
505
+ continue;
506
+ }
417
507
 
418
- knownIds.add(result.newId);
508
+ // Capture the getSchedule response immediately after adding (before dismiss).
509
+ const result = await captureNewSchedule(knownIds);
419
510
 
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
- }
511
+ // Remove the room so "Add a room" reappears for the next room.
512
+ await dismissRoom();
513
+
514
+ if (!result.newId) {
515
+ allRows.push({
516
+ roomName: r.name, roomEmail: r.email || '', date,
517
+ state: 'error', start: `No free/busy returned for "${r.name}" on ${date}`,
518
+ end: '', durationMin: 0,
519
+ });
520
+ continue;
521
+ }
428
522
 
429
- const timeline = buildTimeline(result.items, date);
430
- for (const r of timeline) {
431
- allRows.push({ roomName: room, roomEmail: result.newId, ...r });
523
+ knownIds.add(result.newId);
524
+
525
+ if (result.errMsg && !result.sawView) {
526
+ allRows.push({
527
+ roomName: r.name, roomEmail: result.newId, date,
528
+ state: 'error', start: `Free/busy error: ${result.errMsg}`,
529
+ end: '', durationMin: 0,
530
+ });
531
+ continue;
532
+ }
533
+
534
+ const timeline = buildTimeline(result.items, date);
535
+ for (const row of timeline) {
536
+ allRows.push({ roomName: r.name, roomEmail: result.newId, ...row });
537
+ }
432
538
  }
433
539
  }
434
540
 
435
541
  if (allRows.length === 0) {
436
- throw new Error(`No results for any of the ${rooms.length} room(s) on ${date}`);
542
+ throw new Error(`No results for any of the ${parsedRooms.length} room(s) on ${date}`);
437
543
  }
438
544
 
439
545
  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.0",
4
4
  "description": "Enterprise browser automation CLI",
5
5
  "type": "module",
6
6
  "license": "MIT",