checkbox-cli 3.0.1 → 3.2.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 (2) hide show
  1. package/dist/checkbox.js +684 -88
  2. package/package.json +1 -1
package/dist/checkbox.js CHANGED
@@ -16,7 +16,7 @@ import { join } from 'path';
16
16
  // ── Config ──────────────────────────────────────────────────────────
17
17
  const CONFIG_DIR = join(homedir(), '.checkbox');
18
18
  const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
19
- const VERSION = '3.0.1';
19
+ const VERSION = '3.2.0';
20
20
  function loadConfig() {
21
21
  try {
22
22
  if (!existsSync(CONFIG_FILE))
@@ -194,6 +194,37 @@ async function pickEntity(post, orgId, entityType, label, filters) {
194
194
  return null;
195
195
  }
196
196
  }
197
+ async function pickMember(post, orgId, label = 'team members') {
198
+ const s = p.spinner();
199
+ s.start(`Loading ${label}...`);
200
+ try {
201
+ const members = await listEntities(post, orgId, 'member');
202
+ s.stop(`${members.length} ${label}`);
203
+ if (members.length === 0) {
204
+ p.log.info(`No ${label} found.`);
205
+ return null;
206
+ }
207
+ const selected = await p.select({
208
+ message: `Choose a person`,
209
+ options: [
210
+ ...members.slice(0, 50).map((m) => ({
211
+ value: m.id,
212
+ label: m.name || m.metadata?.email || 'Unknown',
213
+ hint: m.metadata?.role || '',
214
+ })),
215
+ { value: '__back', label: pc.dim('\u2190 Back') },
216
+ ],
217
+ });
218
+ if (p.isCancel(selected) || selected === '__back')
219
+ return null;
220
+ return members.find((m) => m.id === selected) || null;
221
+ }
222
+ catch (err) {
223
+ s.stop(pc.red('Failed'));
224
+ p.log.error(err.message || `Could not load ${label}`);
225
+ return null;
226
+ }
227
+ }
197
228
  async function txt(message, required = false, placeholder) {
198
229
  const val = await p.text({
199
230
  message,
@@ -480,40 +511,156 @@ async function mTasks(cfg, post) {
480
511
  if (!frequency)
481
512
  return mTasks(cfg, post);
482
513
  const payload = { name, frequency };
514
+ // Frequency configuration (day of week, day of month, etc.)
515
+ if (frequency === 'weekly') {
516
+ const dow = await sel('Which day of the week?', [
517
+ { value: 'monday', label: 'Monday' }, { value: 'tuesday', label: 'Tuesday' },
518
+ { value: 'wednesday', label: 'Wednesday' }, { value: 'thursday', label: 'Thursday' },
519
+ { value: 'friday', label: 'Friday' }, { value: 'saturday', label: 'Saturday' },
520
+ { value: 'sunday', label: 'Sunday' },
521
+ ]);
522
+ if (dow)
523
+ payload.frequency_config = { day_of_week: dow };
524
+ }
525
+ else if (frequency === 'monthly') {
526
+ const dom = await txt('Which day of the month? (1-31)', false, '1');
527
+ if (dom)
528
+ payload.frequency_config = { day_of_month: parseInt(dom, 10) };
529
+ }
530
+ else if (frequency === 'yearly') {
531
+ const month = await txt('Month (1-12)', false, '1');
532
+ const day = await txt('Day (1-31)', false, '1');
533
+ if (month && day)
534
+ payload.frequency_config = { month: parseInt(month, 10), day: parseInt(day, 10) };
535
+ }
483
536
  const addPlan = await yn('Add to a plan?');
484
537
  if (addPlan) {
485
538
  const plan = await pickEntity(post, cfg.orgId, 'plan', 'plans');
486
- if (plan)
539
+ if (plan) {
487
540
  payload.plan_id = plan.id;
541
+ // Optionally link to a specific requirement
542
+ const linkReq = await yn('Link to a specific requirement?');
543
+ if (linkReq) {
544
+ const req = await pickEntity(post, cfg.orgId, 'requirement', 'requirements');
545
+ if (req)
546
+ payload.requirement_id = req.id;
547
+ }
548
+ }
488
549
  }
489
- const more = await yn('Add more details? (description, time, reminders, etc.)');
490
- if (more) {
491
- const desc = await txt('Description');
492
- if (desc)
493
- payload.description = desc;
494
- const priv = await yn('Private task?');
495
- if (priv)
496
- payload.is_private = true;
497
- const time = await txt('Scheduled time (HH:MM)', false, '09:00');
498
- if (time)
499
- payload.time = time;
500
- const reminder = await sel('Reminder', [
501
- { value: 'none', label: 'No reminder' },
502
- { value: 'notification', label: 'Push notification' },
503
- { value: 'email', label: 'Email' },
504
- ]);
505
- if (reminder && reminder !== 'none') {
506
- payload.reminder_mode = reminder;
507
- const mins = await txt('Minutes before', false, '30');
508
- if (mins)
509
- payload.reminder_minutes = parseInt(mins, 10);
550
+ // Description
551
+ const desc = await txt('Description (blank to skip)');
552
+ if (desc)
553
+ payload.description = desc;
554
+ // Time
555
+ const time = await txt('Scheduled time (HH:MM, blank to skip)', false, '09:00');
556
+ if (time)
557
+ payload.time = time;
558
+ // Assignment
559
+ const assign = await yn('Assign to a specific person?');
560
+ if (assign) {
561
+ const member = await pickMember(post, cfg.orgId);
562
+ if (member) {
563
+ payload.assigned_to = member.id;
564
+ payload.assigned_to_name = member.name;
510
565
  }
511
- const approval = await yn('Requires approval?');
512
- if (approval)
513
- payload.approval_required = true;
514
- const cond = await txt('Conditional question (blank to skip)', false, 'e.g. Is the equipment available?');
515
- if (cond)
516
- payload.conditional_question = cond;
566
+ }
567
+ // Privacy
568
+ const priv = await yn('Private task?');
569
+ if (priv)
570
+ payload.is_private = true;
571
+ // Reminder
572
+ const reminder = await sel('Reminder', [
573
+ { value: 'none', label: 'No reminder' },
574
+ { value: 'notification', label: 'Push notification' },
575
+ { value: 'email', label: 'Email' },
576
+ ]);
577
+ if (reminder && reminder !== 'none') {
578
+ payload.reminder_mode = reminder;
579
+ const mins = await txt('Minutes before', false, '30');
580
+ if (mins)
581
+ payload.reminder_minutes = parseInt(mins, 10);
582
+ }
583
+ // Conditional question
584
+ const cond = await txt('Conditional question (blank to skip)', false, 'e.g. Is the equipment available?');
585
+ if (cond)
586
+ payload.conditional_question = cond;
587
+ // Approval requirement
588
+ const approval = await yn('Requires approval before completion?');
589
+ if (approval)
590
+ payload.approval_required = true;
591
+ // Proof / Validation requirements (THE MOST IMPORTANT FEATURE)
592
+ const addProof = await yn('Require proof to complete this task? (photos, documents, location, etc.)');
593
+ if (addProof) {
594
+ const validationTypes = [];
595
+ let addMoreProof = true;
596
+ while (addMoreProof) {
597
+ const proofType = await sel('What type of proof?', [
598
+ { value: 'photo', label: 'Photo', hint: 'require a photo to be taken' },
599
+ { value: 'document', label: 'Document', hint: 'require a document upload' },
600
+ { value: 'geolocation', label: 'Location check-in', hint: 'require being at a specific location' },
601
+ { value: 'checklist', label: 'Checklist', hint: 'require completing a checklist' },
602
+ { value: 'table', label: 'Table record', hint: 'require filling out a data table' },
603
+ ]);
604
+ if (!proofType)
605
+ break;
606
+ const vt = { type: proofType };
607
+ // Pick the specific asset for this proof type
608
+ if (proofType === 'photo') {
609
+ const ph = await pickEntity(post, cfg.orgId, 'photo', 'photo assets');
610
+ if (ph)
611
+ vt.config_id = ph.id;
612
+ }
613
+ else if (proofType === 'document') {
614
+ const doc = await pickEntity(post, cfg.orgId, 'document', 'document folders');
615
+ if (doc) {
616
+ vt.config_id = doc.id;
617
+ const targetMode = await sel('Where should uploads go?', [
618
+ { value: 'locked', label: 'Always to this folder' },
619
+ { value: 'user_choice', label: 'Let user choose folder' },
620
+ ]);
621
+ if (targetMode) {
622
+ vt.target_mode = targetMode;
623
+ if (targetMode === 'locked')
624
+ vt.target_folder_id = doc.id;
625
+ }
626
+ }
627
+ }
628
+ else if (proofType === 'geolocation') {
629
+ const area = await pickEntity(post, cfg.orgId, 'geolocation_area', 'location areas');
630
+ if (area)
631
+ vt.config_id = area.id;
632
+ }
633
+ else if (proofType === 'checklist') {
634
+ const cl = await pickEntity(post, cfg.orgId, 'checklist', 'checklists');
635
+ if (cl)
636
+ vt.config_id = cl.id;
637
+ }
638
+ else if (proofType === 'table') {
639
+ const tbl = await pickEntity(post, cfg.orgId, 'table', 'tables');
640
+ if (tbl)
641
+ vt.config_id = tbl.id;
642
+ }
643
+ validationTypes.push(vt);
644
+ addMoreProof = !!(await yn('Add another proof requirement?'));
645
+ }
646
+ if (validationTypes.length > 0)
647
+ payload.validation_types = validationTypes;
648
+ }
649
+ // Share with specific users
650
+ const share = await yn('Share with specific users?');
651
+ if (share) {
652
+ const shareIds = [];
653
+ let addMore = true;
654
+ while (addMore) {
655
+ const member = await pickMember(post, cfg.orgId);
656
+ if (member)
657
+ shareIds.push(member.id);
658
+ else
659
+ break;
660
+ addMore = !!(await yn('Add another person?'));
661
+ }
662
+ if (shareIds.length > 0)
663
+ payload.shared_with_ids = shareIds;
517
664
  }
518
665
  await cmd(post, cfg.orgId, 'create_task', payload, 'Creating task...');
519
666
  return mTasks(cfg, post);
@@ -522,24 +669,131 @@ async function mTasks(cfg, post) {
522
669
  const t = await pickEntity(post, cfg.orgId, 'task', 'tasks');
523
670
  if (!t)
524
671
  return mTasks(cfg, post);
525
- p.log.info(`Editing: ${pc.cyan(t.name)} ${pc.dim('(leave blank to skip)')}`);
526
- const updates = {};
527
- const n = await txt('New name');
528
- if (n === null)
529
- return mTasks(cfg, post);
530
- if (n)
531
- updates.name = n;
532
- const f = await sel('New frequency', [
533
- { value: '__skip', label: pc.dim('Keep current') },
534
- { value: 'daily', label: 'Daily' }, { value: 'weekly', label: 'Weekly' },
535
- { value: 'monthly', label: 'Monthly' }, { value: 'yearly', label: 'Yearly' },
536
- { value: 'one-time', label: 'One time' },
672
+ p.log.info(`Editing: ${pc.cyan(t.name)}`);
673
+ const field = await sel('What would you like to change?', [
674
+ { value: 'name', label: 'Name' },
675
+ { value: 'frequency', label: 'Frequency' },
676
+ { value: 'description', label: 'Description' },
677
+ { value: 'time', label: 'Scheduled time' },
678
+ { value: 'assigned_to', label: 'Assignment' },
679
+ { value: 'is_private', label: 'Privacy' },
680
+ { value: 'reminder', label: 'Reminder settings' },
681
+ { value: 'conditional_question', label: 'Conditional question' },
682
+ { value: 'plan', label: 'Plan / requirement link' },
683
+ { value: '__back', label: pc.dim('\u2190 Back') },
537
684
  ]);
538
- if (f && f !== '__skip')
539
- updates.frequency = f;
540
- const d = await txt('New description');
541
- if (d)
542
- updates.description = d;
685
+ if (!field || field === '__back')
686
+ return mTasks(cfg, post);
687
+ const updates = {};
688
+ switch (field) {
689
+ case 'name': {
690
+ const n = await txt('New name', true);
691
+ if (n)
692
+ updates.name = n;
693
+ break;
694
+ }
695
+ case 'frequency': {
696
+ const f = await sel('New frequency', [
697
+ { value: 'daily', label: 'Daily' }, { value: 'weekly', label: 'Weekly' },
698
+ { value: 'monthly', label: 'Monthly' }, { value: 'yearly', label: 'Yearly' },
699
+ { value: 'one-time', label: 'One time' },
700
+ ]);
701
+ if (f) {
702
+ updates.frequency = f;
703
+ if (f === 'weekly') {
704
+ const dow = await sel('Which day?', [
705
+ { value: 'monday', label: 'Monday' }, { value: 'tuesday', label: 'Tuesday' },
706
+ { value: 'wednesday', label: 'Wednesday' }, { value: 'thursday', label: 'Thursday' },
707
+ { value: 'friday', label: 'Friday' }, { value: 'saturday', label: 'Saturday' },
708
+ { value: 'sunday', label: 'Sunday' },
709
+ ]);
710
+ if (dow)
711
+ updates.frequency_config = { day_of_week: dow };
712
+ }
713
+ else if (f === 'monthly') {
714
+ const dom = await txt('Which day of the month? (1-31)', false, '1');
715
+ if (dom)
716
+ updates.frequency_config = { day_of_month: parseInt(dom, 10) };
717
+ }
718
+ }
719
+ break;
720
+ }
721
+ case 'description': {
722
+ const d = await txt('New description', true);
723
+ if (d)
724
+ updates.description = d;
725
+ break;
726
+ }
727
+ case 'time': {
728
+ const time = await txt('New scheduled time (HH:MM)', false, '09:00');
729
+ if (time)
730
+ updates.time = time;
731
+ break;
732
+ }
733
+ case 'assigned_to': {
734
+ const unassign = await yn('Assign to someone? (No to unassign)');
735
+ if (unassign) {
736
+ const member = await pickMember(post, cfg.orgId);
737
+ if (member) {
738
+ updates.assigned_to = member.id;
739
+ updates.assigned_to_name = member.name;
740
+ }
741
+ }
742
+ else {
743
+ updates.assigned_to = null;
744
+ updates.assigned_to_name = null;
745
+ }
746
+ break;
747
+ }
748
+ case 'is_private': {
749
+ const priv = await yn('Make this task private?');
750
+ updates.is_private = !!priv;
751
+ break;
752
+ }
753
+ case 'reminder': {
754
+ const mode = await sel('Reminder type', [
755
+ { value: 'none', label: 'No reminder' },
756
+ { value: 'notification', label: 'Push notification' },
757
+ { value: 'email', label: 'Email' },
758
+ ]);
759
+ if (mode === 'none') {
760
+ updates.reminder_mode = null;
761
+ updates.reminder_minutes = null;
762
+ }
763
+ else if (mode) {
764
+ updates.reminder_mode = mode;
765
+ const mins = await txt('Minutes before', false, '30');
766
+ if (mins)
767
+ updates.reminder_minutes = parseInt(mins, 10);
768
+ }
769
+ break;
770
+ }
771
+ case 'conditional_question': {
772
+ const q = await txt('Conditional question (blank to remove)');
773
+ updates.conditional_question = q || null;
774
+ break;
775
+ }
776
+ case 'plan': {
777
+ const plan = await pickEntity(post, cfg.orgId, 'plan', 'plans');
778
+ if (plan) {
779
+ updates.plan_id = plan.id;
780
+ const linkReq = await yn('Link to a requirement?');
781
+ if (linkReq) {
782
+ const req = await pickEntity(post, cfg.orgId, 'requirement', 'requirements');
783
+ if (req)
784
+ updates.requirement_id = req.id;
785
+ }
786
+ }
787
+ else {
788
+ const unlink = await yn('Remove from current plan?');
789
+ if (unlink) {
790
+ updates.plan_id = null;
791
+ updates.requirement_id = null;
792
+ }
793
+ }
794
+ break;
795
+ }
796
+ }
543
797
  if (Object.keys(updates).length > 0) {
544
798
  await cmd(post, cfg.orgId, 'update_task', { task_id: t.id, updates }, 'Updating...');
545
799
  }
@@ -633,9 +887,9 @@ async function mTasks(cfg, post) {
633
887
  const payload = { task_id: t.id };
634
888
  const specify = await yn('Specify an approver?');
635
889
  if (specify) {
636
- const aid = await txt('Approver user ID', true);
637
- if (aid)
638
- payload.approver_id = aid;
890
+ const member = await pickMember(post, cfg.orgId);
891
+ if (member)
892
+ payload.approver_id = member.id;
639
893
  }
640
894
  await cmd(post, cfg.orgId, 'request_approval', payload, 'Requesting approval...');
641
895
  return mTasks(cfg, post);
@@ -655,9 +909,9 @@ async function mTasks(cfg, post) {
655
909
  const t = await pickEntity(post, cfg.orgId, 'task', 'tasks');
656
910
  if (!t)
657
911
  return mTasks(cfg, post);
658
- const rid = await txt('Who should get the reminder? (user ID)', true);
659
- if (rid)
660
- await cmd(post, cfg.orgId, 'send_nudge', { task_id: t.id, receiver_id: rid }, 'Sending reminder...');
912
+ const member = await pickMember(post, cfg.orgId);
913
+ if (member)
914
+ await cmd(post, cfg.orgId, 'send_nudge', { task_id: t.id, receiver_id: member.id }, `Sending reminder to ${member.name}...`);
661
915
  return mTasks(cfg, post);
662
916
  }
663
917
  case 'corrective': {
@@ -1009,14 +1263,67 @@ async function mTables(cfg, post) {
1009
1263
  if (!fn)
1010
1264
  break;
1011
1265
  const ft = await sel('Column type', [
1012
- { value: 'text', label: 'Text' }, { value: 'number', label: 'Number' },
1013
- { value: 'date', label: 'Date' }, { value: 'select', label: 'Dropdown' },
1014
- { value: 'checkbox', label: 'Checkbox' }, { value: 'file', label: 'File' },
1266
+ { value: 'text', label: 'Text' },
1267
+ { value: 'number', label: 'Number' },
1268
+ { value: 'date', label: 'Date' },
1269
+ { value: 'select', label: 'Dropdown', hint: 'pick from a list of values' },
1270
+ { value: 'multi_select', label: 'Multi-select dropdown', hint: 'pick multiple values' },
1271
+ { value: 'checkbox', label: 'Checkbox', hint: 'yes/no toggle' },
1272
+ { value: 'file', label: 'File upload' },
1273
+ { value: 'url', label: 'URL / link' },
1274
+ { value: 'email', label: 'Email address' },
1275
+ { value: 'phone', label: 'Phone number' },
1276
+ { value: 'currency', label: 'Currency / money' },
1277
+ { value: 'percent', label: 'Percentage' },
1278
+ { value: 'rating', label: 'Rating (1-5)' },
1279
+ { value: 'relation', label: 'Link to another table' },
1015
1280
  ]);
1016
1281
  if (!ft)
1017
1282
  break;
1283
+ const col = { name: fn, type: ft };
1018
1284
  const req = await yn('Required?');
1019
- fields.push({ name: fn, type: ft, ...(req ? { required: true } : {}) });
1285
+ if (req)
1286
+ col.required = true;
1287
+ // Dropdown values
1288
+ if (ft === 'select' || ft === 'multi_select') {
1289
+ const optSource = await sel('Where do the options come from?', [
1290
+ { value: 'manual', label: 'I\'ll type them now' },
1291
+ { value: 'list', label: 'Link to an existing table/list' },
1292
+ ]);
1293
+ if (optSource === 'manual') {
1294
+ const optStr = await txt('Options (comma-separated)', true, 'e.g. Pass, Fail, N/A');
1295
+ if (optStr)
1296
+ col.options = optStr.split(',').map(s => s.trim()).filter(Boolean);
1297
+ }
1298
+ else if (optSource === 'list') {
1299
+ const srcTable = await pickEntity(post, cfg.orgId, 'table', 'tables');
1300
+ if (srcTable) {
1301
+ col.source_table_id = srcTable.id;
1302
+ const srcField = await txt('Which column to use as the option label?', false, 'name');
1303
+ if (srcField)
1304
+ col.source_field = srcField;
1305
+ }
1306
+ }
1307
+ const defaultVal = await txt('Default value (blank for none)');
1308
+ if (defaultVal)
1309
+ col.default = defaultVal;
1310
+ }
1311
+ // Number/currency constraints
1312
+ if (ft === 'number' || ft === 'currency' || ft === 'percent') {
1313
+ const min = await txt('Minimum value (blank for no limit)');
1314
+ if (min)
1315
+ col.min = parseFloat(min);
1316
+ const max = await txt('Maximum value (blank for no limit)');
1317
+ if (max)
1318
+ col.max = parseFloat(max);
1319
+ }
1320
+ // Relation target
1321
+ if (ft === 'relation') {
1322
+ const relTable = await pickEntity(post, cfg.orgId, 'table', 'tables');
1323
+ if (relTable)
1324
+ col.relation_table_id = relTable.id;
1325
+ }
1326
+ fields.push(col);
1020
1327
  addMore = !!(await yn('Add another column?'));
1021
1328
  }
1022
1329
  if (fields.length > 0)
@@ -1510,20 +1817,37 @@ async function mAutomation(cfg, post) {
1510
1817
  return mAutomation(cfg, post);
1511
1818
  }
1512
1819
  case 'val-create': {
1513
- const task = await pickEntity(post, cfg.orgId, 'task', 'tasks');
1514
- if (!task)
1820
+ // Choose target: task or requirement
1821
+ const target = await sel('Apply this rule to', [
1822
+ { value: 'task', label: 'A specific task' },
1823
+ { value: 'requirement', label: 'A requirement', hint: 'applies to all tasks under it' },
1824
+ ]);
1825
+ if (!target)
1515
1826
  return mAutomation(cfg, post);
1827
+ const rulePayload = {};
1828
+ if (target === 'task') {
1829
+ const task = await pickEntity(post, cfg.orgId, 'task', 'tasks');
1830
+ if (!task)
1831
+ return mAutomation(cfg, post);
1832
+ rulePayload.task_id = task.id;
1833
+ }
1834
+ else {
1835
+ const req = await pickEntity(post, cfg.orgId, 'requirement', 'requirements');
1836
+ if (!req)
1837
+ return mAutomation(cfg, post);
1838
+ rulePayload.requirement_id = req.id;
1839
+ }
1516
1840
  const name = await txt('Rule name', true, 'e.g. Require photo proof');
1517
1841
  if (!name)
1518
1842
  return mAutomation(cfg, post);
1519
1843
  const ruleType = await sel('Rule type', [
1520
- { value: 'proof_required', label: 'Proof required', hint: 'photo, document, location, etc.' },
1521
- { value: 'field_constraint', label: 'Field constraint', hint: 'validate a data field' },
1844
+ { value: 'proof_required', label: 'Proof required', hint: 'photo, document, location, checklist, or table' },
1845
+ { value: 'field_constraint', label: 'Field constraint', hint: 'validate a specific field value' },
1846
+ { value: 'cross_field', label: 'Cross-field validation', hint: 'compare two fields' },
1847
+ { value: 'aggregate', label: 'Aggregate check', hint: 'count/sum/avg of historical data' },
1522
1848
  { value: 'approval_required', label: 'Approval required', hint: 'needs manager sign-off' },
1523
1849
  { value: 'time_window', label: 'Time window', hint: 'must be done within a time range' },
1524
- { value: 'record_exists', label: 'Record exists', hint: 'verify data was entered' },
1525
- { value: 'cross_field', label: 'Cross-field validation' },
1526
- { value: 'aggregate', label: 'Aggregate check' },
1850
+ { value: 'record_exists', label: 'Record exists', hint: 'verify a table record was created' },
1527
1851
  ]);
1528
1852
  if (!ruleType)
1529
1853
  return mAutomation(cfg, post);
@@ -1531,21 +1855,201 @@ async function mAutomation(cfg, post) {
1531
1855
  if (ruleType === 'proof_required') {
1532
1856
  const proofType = await sel('What kind of proof?', [
1533
1857
  { value: 'photo', label: 'Photo' }, { value: 'document', label: 'Document' },
1534
- { value: 'geolocation', label: 'Location' }, { value: 'checklist', label: 'Checklist' },
1858
+ { value: 'geolocation', label: 'Location check-in' }, { value: 'checklist', label: 'Checklist' },
1859
+ { value: 'table', label: 'Table record' },
1535
1860
  ]);
1536
- if (proofType)
1537
- condition.proof_type = proofType;
1861
+ if (proofType) {
1862
+ condition.validation_type = proofType;
1863
+ // Let them pick the specific asset
1864
+ if (proofType === 'photo') {
1865
+ const ph = await pickEntity(post, cfg.orgId, 'photo', 'photo assets');
1866
+ if (ph)
1867
+ condition.config_id = ph.id;
1868
+ }
1869
+ else if (proofType === 'document') {
1870
+ const doc = await pickEntity(post, cfg.orgId, 'document', 'document folders');
1871
+ if (doc) {
1872
+ condition.config_id = doc.id;
1873
+ const tMode = await sel('Upload destination', [
1874
+ { value: 'locked', label: 'Always to this folder' },
1875
+ { value: 'user_choice', label: 'Let user choose' },
1876
+ ]);
1877
+ if (tMode) {
1878
+ condition.target_mode = tMode;
1879
+ if (tMode === 'locked')
1880
+ condition.target_folder_id = doc.id;
1881
+ }
1882
+ }
1883
+ }
1884
+ else if (proofType === 'geolocation') {
1885
+ const area = await pickEntity(post, cfg.orgId, 'geolocation_area', 'location areas');
1886
+ if (area)
1887
+ condition.config_id = area.id;
1888
+ }
1889
+ else if (proofType === 'checklist') {
1890
+ const cl = await pickEntity(post, cfg.orgId, 'checklist', 'checklists');
1891
+ if (cl)
1892
+ condition.config_id = cl.id;
1893
+ }
1894
+ else if (proofType === 'table') {
1895
+ const tbl = await pickEntity(post, cfg.orgId, 'table', 'tables');
1896
+ if (tbl)
1897
+ condition.config_id = tbl.id;
1898
+ }
1899
+ }
1900
+ }
1901
+ else if (ruleType === 'field_constraint') {
1902
+ const field = await txt('Field name', true, 'e.g. status, temperature, score');
1903
+ if (field)
1904
+ condition.field = field;
1905
+ const op = await sel('Condition', [
1906
+ { value: 'eq', label: 'Equals' }, { value: 'neq', label: 'Not equals' },
1907
+ { value: 'gt', label: 'Greater than' }, { value: 'gte', label: 'Greater than or equal' },
1908
+ { value: 'lt', label: 'Less than' }, { value: 'lte', label: 'Less than or equal' },
1909
+ { value: 'in', label: 'Is one of (list)' }, { value: 'not_in', label: 'Is not one of (list)' },
1910
+ { value: 'is_null', label: 'Is empty' }, { value: 'is_not_null', label: 'Is not empty' },
1911
+ { value: 'matches', label: 'Matches pattern' },
1912
+ ]);
1913
+ if (op)
1914
+ condition.operator = op;
1915
+ if (op && !['is_null', 'is_not_null'].includes(op)) {
1916
+ if (op === 'in' || op === 'not_in') {
1917
+ const vals = await txt('Values (comma-separated)', true);
1918
+ if (vals)
1919
+ condition.value = vals.split(',').map(s => s.trim());
1920
+ }
1921
+ else {
1922
+ const val = await txt('Value', true);
1923
+ if (val)
1924
+ condition.value = isNaN(Number(val)) ? val : Number(val);
1925
+ }
1926
+ }
1927
+ }
1928
+ else if (ruleType === 'cross_field') {
1929
+ const f1 = await txt('First field', true);
1930
+ if (f1)
1931
+ condition.field = f1;
1932
+ const cop = await sel('Must be', [
1933
+ { value: 'eq', label: 'Equal to' }, { value: 'neq', label: 'Not equal to' },
1934
+ { value: 'gt', label: 'Greater than' }, { value: 'gte', label: 'Greater or equal to' },
1935
+ { value: 'lt', label: 'Less than' }, { value: 'lte', label: 'Less or equal to' },
1936
+ ]);
1937
+ if (cop)
1938
+ condition.compare_operator = cop;
1939
+ const f2 = await txt('Second field', true);
1940
+ if (f2)
1941
+ condition.compare_field = f2;
1942
+ }
1943
+ else if (ruleType === 'aggregate') {
1944
+ const aggFn = await sel('Aggregate function', [
1945
+ { value: 'count', label: 'Count' }, { value: 'sum', label: 'Sum' },
1946
+ { value: 'avg', label: 'Average' }, { value: 'min', label: 'Minimum' },
1947
+ { value: 'max', label: 'Maximum' },
1948
+ ]);
1949
+ if (aggFn)
1950
+ condition.aggregate_function = aggFn;
1951
+ condition.aggregate_table = 'task_history';
1952
+ if (aggFn !== 'count') {
1953
+ const aggField = await txt('Field to aggregate', true);
1954
+ if (aggField)
1955
+ condition.aggregate_field = aggField;
1956
+ }
1957
+ const aggOp = await sel('Result must be', [
1958
+ { value: 'eq', label: 'Equal to' }, { value: 'gt', label: 'Greater than' },
1959
+ { value: 'gte', label: 'At least' }, { value: 'lt', label: 'Less than' },
1960
+ { value: 'lte', label: 'At most' },
1961
+ ]);
1962
+ if (aggOp)
1963
+ condition.aggregate_operator = aggOp;
1964
+ const aggVal = await txt('Target value', true, 'e.g. 5');
1965
+ if (aggVal)
1966
+ condition.aggregate_value = parseFloat(aggVal);
1967
+ }
1968
+ else if (ruleType === 'approval_required') {
1969
+ const approverType = await sel('Who must approve?', [
1970
+ { value: 'role', label: 'Anyone with a specific role' },
1971
+ { value: 'user', label: 'A specific person' },
1972
+ ]);
1973
+ if (approverType === 'role') {
1974
+ const role = await sel('Required role', [
1975
+ { value: 'admin', label: 'Admin' }, { value: 'super_admin', label: 'Super Admin' },
1976
+ ]);
1977
+ if (role)
1978
+ condition.approval_approver_role = role;
1979
+ }
1980
+ else if (approverType === 'user') {
1981
+ const member = await pickMember(post, cfg.orgId);
1982
+ if (member)
1983
+ condition.approval_approver_id = member.id;
1984
+ }
1538
1985
  }
1539
1986
  else if (ruleType === 'time_window') {
1540
- const start = await txt('Start time (HH:MM)', false, '08:00');
1541
- const end = await txt('End time (HH:MM)', false, '17:00');
1987
+ const wType = await sel('Window type', [
1988
+ { value: 'daily', label: 'Daily time window' },
1989
+ { value: 'custom', label: 'Custom date/time range' },
1990
+ ]);
1991
+ if (wType)
1992
+ condition.window_type = wType;
1993
+ const start = await txt('Start time (HH:MM)', true, '08:00');
1542
1994
  if (start)
1543
1995
  condition.start_time = start;
1996
+ const end = await txt('End time (HH:MM)', true, '17:00');
1544
1997
  if (end)
1545
1998
  condition.end_time = end;
1999
+ const tz = await txt('Timezone (blank for UTC)', false, 'America/New_York');
2000
+ if (tz)
2001
+ condition.timezone = tz;
2002
+ }
2003
+ else if (ruleType === 'record_exists') {
2004
+ const tbl = await pickEntity(post, cfg.orgId, 'table', 'tables');
2005
+ if (tbl) {
2006
+ condition.table = tbl.id;
2007
+ const addFilters = await yn('Add filters? (only match records with specific values)');
2008
+ if (addFilters) {
2009
+ const filters = [];
2010
+ let addF = true;
2011
+ while (addF) {
2012
+ const ff = await txt('Field name', true);
2013
+ if (!ff)
2014
+ break;
2015
+ const fop = await sel('Condition', [
2016
+ { value: 'eq', label: 'Equals' }, { value: 'neq', label: 'Not equals' },
2017
+ { value: 'gt', label: 'Greater than' }, { value: 'lt', label: 'Less than' },
2018
+ { value: 'is_not_null', label: 'Has a value' },
2019
+ ]);
2020
+ if (!fop)
2021
+ break;
2022
+ if (fop === 'is_not_null') {
2023
+ filters.push({ field: ff, operator: fop, value: null });
2024
+ }
2025
+ else {
2026
+ const fv = await txt('Value', true);
2027
+ if (fv)
2028
+ filters.push({ field: ff, operator: fop, value: isNaN(Number(fv)) ? fv : Number(fv) });
2029
+ }
2030
+ addF = !!(await yn('Add another filter?'));
2031
+ }
2032
+ if (filters.length > 0)
2033
+ condition.filters = filters;
2034
+ }
2035
+ }
2036
+ }
2037
+ // Grouping options
2038
+ const addGroup = await yn('Add to a rule group? (for AND/OR logic with other rules)');
2039
+ if (addGroup) {
2040
+ const gid = await txt('Group ID (create a new UUID or use an existing group ID)', true);
2041
+ if (gid) {
2042
+ rulePayload.group_id = gid;
2043
+ const gop = await sel('Group logic', [
2044
+ { value: 'AND', label: 'AND — all rules in the group must pass' },
2045
+ { value: 'OR', label: 'OR — any rule in the group can pass' },
2046
+ ]);
2047
+ if (gop)
2048
+ rulePayload.group_operator = gop;
2049
+ }
1546
2050
  }
1547
2051
  await cmd(post, cfg.orgId, 'create_validation_rule', {
1548
- task_id: task.id, name, rule_type: ruleType, condition,
2052
+ ...rulePayload, name, rule_type: ruleType, condition,
1549
2053
  }, 'Creating validation rule...');
1550
2054
  return mAutomation(cfg, post);
1551
2055
  }
@@ -1621,25 +2125,100 @@ async function mAutomation(cfg, post) {
1621
2125
  if (!name)
1622
2126
  return mAutomation(cfg, post);
1623
2127
  const eventType = await sel('What triggers this?', [
1624
- { value: 'record_created', label: 'New record added to a table' },
1625
- { value: 'record_updated', label: 'Record updated in a table' },
1626
2128
  { value: 'task_completed', label: 'Another task completed' },
1627
- { value: 'task_missed', label: 'A task was missed' },
1628
- { value: 'approval_resolved', label: 'An approval was resolved' },
2129
+ { value: 'task_uncompleted', label: 'A task was reopened' },
2130
+ { value: 'task_created', label: 'A new task was created' },
2131
+ { value: 'task_updated', label: 'A task was updated' },
2132
+ { value: 'validation_submitted', label: 'Proof was submitted' },
2133
+ { value: 'approval_requested', label: 'An approval was requested' },
2134
+ { value: 'approval_approved', label: 'An approval was granted' },
2135
+ { value: 'approval_rejected', label: 'An approval was rejected' },
2136
+ { value: 'table_record_created', label: 'New record added to a table' },
2137
+ { value: 'conditional_answer', label: 'A conditional question was answered' },
2138
+ { value: 'document_created', label: 'A document was uploaded' },
2139
+ { value: 'checklist_created', label: 'A checklist was created' },
1629
2140
  ]);
1630
2141
  if (!eventType)
1631
2142
  return mAutomation(cfg, post);
1632
- const sourceType = await sel('Source type', [
1633
- { value: 'table', label: 'Table' }, { value: 'task', label: 'Task' },
2143
+ const sourceType = await sel('Source type (what entity fires the event)', [
2144
+ { value: 'task', label: 'Task' },
2145
+ { value: 'table', label: 'Table' },
2146
+ { value: 'document', label: 'Document' },
2147
+ { value: 'checklist', label: 'Checklist' },
2148
+ { value: 'geolocation_area', label: 'Location area' },
1634
2149
  { value: 'approval', label: 'Approval' },
2150
+ { value: 'plan', label: 'Plan' },
1635
2151
  ]);
1636
2152
  if (!sourceType)
1637
2153
  return mAutomation(cfg, post);
1638
- const payload = { task_id: task.id, name, event_type: eventType, source_entity_type: sourceType };
1639
- const source = await pickEntity(post, cfg.orgId, sourceType, `${sourceType}s`);
1640
- if (source)
1641
- payload.source_entity_id = source.id;
1642
- await cmd(post, cfg.orgId, 'create_trigger_rule', payload, 'Creating trigger rule...');
2154
+ const trigPayload = { task_id: task.id, name, event_type: eventType, source_entity_type: sourceType };
2155
+ const pickSource = await yn(`Pick a specific ${sourceType}? (or trigger on any)`);
2156
+ if (pickSource) {
2157
+ const source = await pickEntity(post, cfg.orgId, sourceType, `${sourceType}s`);
2158
+ if (source)
2159
+ trigPayload.source_entity_id = source.id;
2160
+ }
2161
+ // Conditions
2162
+ const addConds = await yn('Add conditions? (only trigger when specific criteria are met)');
2163
+ if (addConds) {
2164
+ const conditions = [];
2165
+ let addC = true;
2166
+ while (addC) {
2167
+ const cf = await txt('Field to check', true, 'e.g. status, frequency, name');
2168
+ if (!cf)
2169
+ break;
2170
+ const cop = await sel('Condition', [
2171
+ { value: 'eq', label: 'Equals' }, { value: 'neq', label: 'Not equals' },
2172
+ { value: 'gt', label: 'Greater than' }, { value: 'lt', label: 'Less than' },
2173
+ { value: 'in', label: 'Is one of' }, { value: 'is_not_null', label: 'Has a value' },
2174
+ ]);
2175
+ if (!cop)
2176
+ break;
2177
+ const csrc = await sel('Check against', [
2178
+ { value: 'payload', label: 'Event data (payload)' },
2179
+ { value: 'entity', label: 'Entity state' },
2180
+ ]);
2181
+ let cv = null;
2182
+ if (cop !== 'is_not_null') {
2183
+ if (cop === 'in') {
2184
+ const cvStr = await txt('Values (comma-separated)', true);
2185
+ if (cvStr)
2186
+ cv = cvStr.split(',').map(s => s.trim());
2187
+ }
2188
+ else {
2189
+ const cvTxt = await txt('Value', true);
2190
+ if (cvTxt)
2191
+ cv = isNaN(Number(cvTxt)) ? cvTxt : Number(cvTxt);
2192
+ }
2193
+ }
2194
+ conditions.push({ field: cf, operator: cop, value: cv, ...(csrc ? { source: csrc } : {}) });
2195
+ addC = !!(await yn('Add another condition?'));
2196
+ }
2197
+ if (conditions.length > 0) {
2198
+ trigPayload.conditions = conditions;
2199
+ if (conditions.length > 1) {
2200
+ const logic = await sel('Condition logic', [
2201
+ { value: 'AND', label: 'ALL conditions must be true' },
2202
+ { value: 'OR', label: 'ANY condition can be true' },
2203
+ ]);
2204
+ if (logic)
2205
+ trigPayload.condition_operator = logic;
2206
+ }
2207
+ }
2208
+ }
2209
+ // Advanced options
2210
+ const advanced = await yn('Configure advanced options? (frequency, message, debounce)');
2211
+ if (advanced) {
2212
+ const respect = await yn('Respect task frequency? (only activate on scheduled days)');
2213
+ trigPayload.respect_frequency = !!respect;
2214
+ const msg = await txt('Activation message (shown when triggered)');
2215
+ if (msg)
2216
+ trigPayload.activation_message = msg;
2217
+ const debounce = await txt('Debounce seconds (prevent re-triggering too fast)', false, '60');
2218
+ if (debounce)
2219
+ trigPayload.debounce_seconds = parseInt(debounce, 10);
2220
+ }
2221
+ await cmd(post, cfg.orgId, 'create_trigger_rule', trigPayload, 'Creating trigger rule...');
1643
2222
  return mAutomation(cfg, post);
1644
2223
  }
1645
2224
  case 'trig-edit': {
@@ -1970,6 +2549,7 @@ async function mApprovals(cfg, post) {
1970
2549
  // ══════════════════════════════════════════════════════════════════════
1971
2550
  async function mTeam(cfg, post) {
1972
2551
  const a = await sel('Team', [
2552
+ { value: 'list', label: 'View team members' },
1973
2553
  { value: 'invite', label: 'Invite a member' },
1974
2554
  { value: 'revoke', label: 'Revoke an invitation' },
1975
2555
  { value: 'role', label: 'Change a member\u2019s role' },
@@ -1979,6 +2559,22 @@ async function mTeam(cfg, post) {
1979
2559
  if (!a || a === '__back')
1980
2560
  return interactiveMenu(cfg);
1981
2561
  switch (a) {
2562
+ case 'list': {
2563
+ const s = p.spinner();
2564
+ s.start('Loading team members...');
2565
+ const members = await listEntities(post, cfg.orgId, 'member');
2566
+ s.stop(`${members.length} team members`);
2567
+ if (members.length > 0) {
2568
+ const lines = members.map((m) => {
2569
+ const name = m.name || 'Unknown';
2570
+ const email = m.metadata?.email || '';
2571
+ const role = m.metadata?.role || '';
2572
+ return ` ${pc.cyan(pad(truncate(name, 30), 32))} ${pc.dim(pad(email, 30))} ${pc.dim(role)}`;
2573
+ });
2574
+ p.note(lines.join('\n'), 'Team Members');
2575
+ }
2576
+ return mTeam(cfg, post);
2577
+ }
1982
2578
  case 'invite': {
1983
2579
  const email = await txt('Email address', true);
1984
2580
  if (!email)
@@ -2001,26 +2597,26 @@ async function mTeam(cfg, post) {
2001
2597
  return mTeam(cfg, post);
2002
2598
  }
2003
2599
  case 'role': {
2004
- const userId = await txt('Member user ID', true);
2005
- if (!userId)
2600
+ const member = await pickMember(post, cfg.orgId);
2601
+ if (!member)
2006
2602
  return mTeam(cfg, post);
2007
- const role = await sel('New role', [
2603
+ const role = await sel(`New role for ${member.name}`, [
2008
2604
  { value: 'user', label: 'Member' },
2009
2605
  { value: 'admin', label: 'Admin' },
2010
2606
  { value: 'super_admin', label: 'Owner' },
2011
2607
  ]);
2012
2608
  if (!role)
2013
2609
  return mTeam(cfg, post);
2014
- await cmd(post, cfg.orgId, 'update_member_role', { user_id: userId, role }, 'Updating role...');
2610
+ await cmd(post, cfg.orgId, 'update_member_role', { user_id: member.id, role }, `Updating ${member.name}'s role...`);
2015
2611
  return mTeam(cfg, post);
2016
2612
  }
2017
2613
  case 'remove': {
2018
- const userId = await txt('Member user ID', true);
2019
- if (!userId)
2614
+ const member = await pickMember(post, cfg.orgId);
2615
+ if (!member)
2020
2616
  return mTeam(cfg, post);
2021
- const sure = await yn('Remove this member? They will lose access to the workspace.');
2617
+ const sure = await yn(`Remove ${member.name}? They will lose access to the workspace.`);
2022
2618
  if (sure)
2023
- await cmd(post, cfg.orgId, 'remove_member', { user_id: userId }, 'Removing member...');
2619
+ await cmd(post, cfg.orgId, 'remove_member', { user_id: member.id }, `Removing ${member.name}...`);
2024
2620
  return mTeam(cfg, post);
2025
2621
  }
2026
2622
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "checkbox-cli",
3
- "version": "3.0.1",
3
+ "version": "3.2.0",
4
4
  "description": "Beautiful, interactive CLI for Checkbox compliance management. Setup wizard, guided workflows, and full workspace control from your terminal.",
5
5
  "type": "module",
6
6
  "main": "dist/checkbox.js",