@dench.com/cli 2.2.0 → 2.2.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.
package/crm.ts CHANGED
@@ -509,6 +509,23 @@ const api = {
509
509
  members: {
510
510
  list: makeFunctionReference<"query">("functions/crm/members:list"),
511
511
  },
512
+ automations: {
513
+ list: makeFunctionReference<"query">("functions/crm/automations:list"),
514
+ setEnabled: makeFunctionReference<"mutation">(
515
+ "functions/crm/automations:setEnabled",
516
+ ),
517
+ },
518
+ templates: {
519
+ listAvailable: makeFunctionReference<"query">(
520
+ "functions/crm/templates:listAvailable",
521
+ ),
522
+ install: makeFunctionReference<"mutation">(
523
+ "functions/crm/templates:install",
524
+ ),
525
+ removeSampleData: makeFunctionReference<"mutation">(
526
+ "functions/crm/templates:removeSampleData",
527
+ ),
528
+ },
512
529
  };
513
530
 
514
531
  export async function runCrmCommand(opts: {
@@ -573,6 +590,10 @@ export async function runCrmCommand(opts: {
573
590
  return await runDocsCommand(ctx);
574
591
  case "actions":
575
592
  return await runActionsCommand(ctx);
593
+ case "automations":
594
+ return await runAutomationsCommand(ctx);
595
+ case "templates":
596
+ return await runTemplatesCommand(ctx);
576
597
  case "members":
577
598
  // Alias for `dench members` so the AI agent finds the roster
578
599
  // surface from inside the `dench crm` namespace it's primed
@@ -584,6 +605,81 @@ export async function runCrmCommand(opts: {
584
605
  }
585
606
  }
586
607
 
608
+ /**
609
+ * `dench crm automations …` — deterministic CRM rules (installed by
610
+ * templates, e.g. "qualified prospect → opportunity"). These fire inside
611
+ * entry writes automatically; this surface is for visibility and the
612
+ * enable/disable kill switch. There is no create verb here — rules are
613
+ * installed by `crm templates install` (or programmatically).
614
+ */
615
+ async function runAutomationsCommand(ctx: CrmCliContext): Promise<void> {
616
+ const verb = ctx.args.shift();
617
+ switch (verb) {
618
+ case "list": {
619
+ assertNoUnknownFlags(ctx.args, "crm automations list");
620
+ out(ctx, await callQuery(ctx, api.automations.list, {}));
621
+ return;
622
+ }
623
+ case "enable":
624
+ case "disable": {
625
+ const automationId = shift(ctx.args, "automation id");
626
+ assertNoUnknownFlags(ctx.args, `crm automations ${verb}`);
627
+ out(
628
+ ctx,
629
+ await callMutation(ctx, api.automations.setEnabled, {
630
+ automationId,
631
+ enabled: verb === "enable",
632
+ }),
633
+ );
634
+ return;
635
+ }
636
+ default:
637
+ throw new CrmCliError(
638
+ `Unknown automations verb: ${verb ?? "(none)"}. Use list | enable <id> | disable <id>.`,
639
+ );
640
+ }
641
+ }
642
+
643
+ /**
644
+ * `dench crm templates …` — one-click CRM setups (objects + fields +
645
+ * kanban statuses + pinned views + automations + sample rows tagged
646
+ * "Sample"). Installs are idempotent and compose across templates.
647
+ */
648
+ async function runTemplatesCommand(ctx: CrmCliContext): Promise<void> {
649
+ const verb = ctx.args.shift();
650
+ switch (verb) {
651
+ case "list": {
652
+ assertNoUnknownFlags(ctx.args, "crm templates list");
653
+ out(ctx, await callQuery(ctx, api.templates.listAvailable, {}));
654
+ return;
655
+ }
656
+ case "install": {
657
+ const templateId = shift(ctx.args, "template id");
658
+ assertNoUnknownFlags(ctx.args, "crm templates install");
659
+ out(
660
+ ctx,
661
+ await callMutation(ctx, api.templates.install, { templateId }),
662
+ );
663
+ return;
664
+ }
665
+ case "remove-sample-data": {
666
+ const templateId = shift(ctx.args, "template id");
667
+ assertNoUnknownFlags(ctx.args, "crm templates remove-sample-data");
668
+ out(
669
+ ctx,
670
+ await callMutation(ctx, api.templates.removeSampleData, {
671
+ templateId,
672
+ }),
673
+ );
674
+ return;
675
+ }
676
+ default:
677
+ throw new CrmCliError(
678
+ `Unknown templates verb: ${verb ?? "(none)"}. Use list | install <id> | remove-sample-data <id>.`,
679
+ );
680
+ }
681
+ }
682
+
587
683
  async function runCrmMembersCommand(ctx: CrmCliContext): Promise<void> {
588
684
  const { runMembersCommand } = await import("./members");
589
685
  // Re-prepend `--json` so the members dispatcher's own `hasFlag`
@@ -634,6 +730,11 @@ async function runObjectsCommand(ctx: CrmCliContext): Promise<void> {
634
730
  const description = getFlag(ctx.args, "--description");
635
731
  const defaultView = getFlag(ctx.args, "--default-view") as any;
636
732
  const icon = getFlag(ctx.args, "--icon");
733
+ // Cosmetic relabels (UI only; canonical `name` is unchanged, so
734
+ // protected objects like `people` can be shown as e.g. "Clients").
735
+ // Pass an empty string to clear back to the default label.
736
+ const displayName = getFlag(ctx.args, "--display-name");
737
+ const displayNameSingular = getFlag(ctx.args, "--display-name-singular");
637
738
  assertNoUnknownFlags(ctx.args, "crm objects update");
638
739
  out(
639
740
  ctx,
@@ -642,6 +743,8 @@ async function runObjectsCommand(ctx: CrmCliContext): Promise<void> {
642
743
  description,
643
744
  defaultView,
644
745
  icon,
746
+ displayName,
747
+ displayNameSingular,
645
748
  }),
646
749
  );
647
750
  return;
@@ -2618,7 +2721,9 @@ Objects (CRM tables):
2618
2721
  dench crm objects list [--json]
2619
2722
  dench crm objects get <name>
2620
2723
  dench crm objects create <name> [--description ...] [--default-view kanban] [--icon ...]
2621
- dench crm objects update <name> [--default-view ...] [--icon ...]
2724
+ dench crm objects update <name> [--default-view ...] [--icon ...] [--display-name "Clients"] [--display-name-singular "Client"]
2725
+ --display-name relabels the UI only; the canonical <name> stays the
2726
+ command/API identifier (works on protected objects too).
2622
2727
  dench crm objects rename <from> <to>
2623
2728
  dench crm objects delete <name>
2624
2729
 
@@ -2743,10 +2848,32 @@ Docs:
2743
2848
  dench crm docs create <path> [--title ...] [--icon ...] [--link-entry <id>]
2744
2849
  dench crm docs link <path> --object <name> --entry <id>
2745
2850
 
2851
+ Templates (one-click CRM setups: objects + automations + sample rows):
2852
+ dench crm templates list [--json]
2853
+ Catalog with per-org installed flags. Installs are idempotent and
2854
+ compose (e.g. sales + fundraising share the company Type enum).
2855
+ dench crm templates install <templateId>
2856
+ dench crm templates remove-sample-data <templateId>
2857
+ Deletes only the template's sample rows (tagged "Sample"); schema,
2858
+ views, and automations stay.
2859
+
2860
+ Automations (deterministic rules, fire inside entry writes):
2861
+ dench crm automations list [--json]
2862
+ e.g. "prospect Status -> Qualified creates an opportunity". Writes
2863
+ that trigger one return an automationEffects array describing what
2864
+ was created/updated — expected behavior, not an error.
2865
+ dench crm automations enable <automationId>
2866
+ dench crm automations disable <automationId>
2867
+
2746
2868
  Actions:
2747
2869
  dench crm actions list <entryId>
2748
2870
  dench crm actions run <actionId> --field-id <id> --entry-id <id>
2749
2871
 
2872
+ Object names: commands take the canonical object name (singular lowercase,
2873
+ e.g. people, company, prospect). Display labels shown in the UI (a template
2874
+ may relabel people as "Clients") and plural forms also resolve when
2875
+ unambiguous, but prefer canonical names from 'crm objects list'.
2876
+
2750
2877
  Global flags:
2751
2878
  --json Output raw JSON instead of pretty-printed text.
2752
2879
  `);
package/email.ts CHANGED
@@ -194,6 +194,8 @@ Usage:
194
194
  dench email identity dns --identity <identityId|domain|email> [--json]
195
195
  dench email identity status --identity founder@example.com [--json]
196
196
  dench email identity status --identity-id <identityId> [--json]
197
+ dench email identity disable --identity <identityId|email|domain> [--json]
198
+ dench email identity restore --identity <identityId|email|domain> [--json]
197
199
  dench email send --from founder@example.com --to "Ada <ada@x.com>,bob@y.com" --subject "Hi" (--html-file body.html | --text "...") [--cc ...] [--bcc ...] [--from-name "Ada"] [--reply-to support@x.com] [--reply-message-id <messageId|rfc-id>] [--json]
198
200
  dench email message status --message <messageId> [--json]
199
201
  dench email message list [--limit 25] [--json]
@@ -465,12 +467,39 @@ async function runIdentity(ctx: EmailCliContext) {
465
467
  (identity) =>
466
468
  `${identity.identityId} ${identity.fromEmail} type=${identity.type}` +
467
469
  `${identity.domain ? ` domain=${identity.domain}` : ""}` +
468
- ` delivery=${identity.deliveryVerificationStatus} workspace=${identity.denchVerificationStatus}`,
470
+ ` delivery=${identity.deliveryVerificationStatus} workspace=${identity.denchVerificationStatus}` +
471
+ `${identity.disabled ? " DISABLED" : ""}`,
469
472
  )
470
473
  .join("\n");
471
474
  out(ctx, { ok: true, identities }, text);
472
475
  return;
473
476
  }
477
+ if (sub === "disable" || sub === "restore") {
478
+ const identity = requiredFlag(ctx.args, "--identity");
479
+ const payload = (await callMutation(
480
+ ctx,
481
+ sub === "disable"
482
+ ? "functions/emailCampaigns:disableIdentity"
483
+ : "functions/emailCampaigns:restoreIdentity",
484
+ { identity },
485
+ )) as JsonRecord;
486
+ const rows = (
487
+ Array.isArray(payload.disabled)
488
+ ? payload.disabled
489
+ : Array.isArray(payload.restored)
490
+ ? payload.restored
491
+ : []
492
+ ) as JsonRecord[];
493
+ const verb = sub === "disable" ? "Disabled" : "Restored";
494
+ const text =
495
+ rows.length === 0
496
+ ? `${verb} 0 identities.`
497
+ : `${verb} ${rows.length} sender(s):\n${rows
498
+ .map((row) => ` ${row.fromEmail} (${row.identityId})`)
499
+ .join("\n")}`;
500
+ out(ctx, payload, text);
501
+ return;
502
+ }
474
503
  if (sub === "dns") {
475
504
  const identity = requiredFlag(ctx.args, "--identity");
476
505
  if (!identity.includes("@") && !identity.includes(".")) {
@@ -509,12 +538,19 @@ async function runIdentity(ctx: EmailCliContext) {
509
538
  : [];
510
539
  const records = buildDnsRecords(domain, tokens);
511
540
  const verified = status.verifiedForSendingStatus === true;
512
- const payload = {
541
+ const payload: JsonRecord = {
513
542
  ok: true,
514
543
  domain,
515
544
  domainVerified: verified,
516
545
  dnsRecords: records,
517
546
  };
547
+ if (
548
+ !verified &&
549
+ status.verificationInfo &&
550
+ typeof status.verificationInfo === "object"
551
+ ) {
552
+ payload.verificationInfo = status.verificationInfo;
553
+ }
518
554
  const text = verified
519
555
  ? `Domain ${domain} is verified — DNS records below are already in place.\n${formatDnsRecords(records)}`
520
556
  : tokens.length > 0
@@ -541,16 +577,27 @@ async function runIdentity(ctx: EmailCliContext) {
541
577
  `/v1/email/identities/${encodeURIComponent(identity)}/status`,
542
578
  );
543
579
  const status = payload as JsonRecord;
544
- const shaped = {
580
+ const shaped: JsonRecord = {
545
581
  ok: status.ok === true,
546
582
  identity: status.identity,
547
583
  deliveryVerified: status.verifiedForSendingStatus === true,
548
584
  deliveryVerificationStatus: status.verificationStatus,
549
585
  };
586
+ // Pending domains: surface when the provider last polled DNS and
587
+ // why it failed, so callers know whether to fix records or wait.
588
+ if (
589
+ status.verifiedForSendingStatus !== true &&
590
+ status.verificationInfo &&
591
+ typeof status.verificationInfo === "object"
592
+ ) {
593
+ shaped.verificationInfo = status.verificationInfo;
594
+ }
550
595
  out(ctx, shaped, JSON.stringify(shaped, null, 2));
551
596
  return;
552
597
  }
553
- throw new EmailCliError("Usage: dench email identity verify|list|dns|status");
598
+ throw new EmailCliError(
599
+ "Usage: dench email identity verify|list|dns|status|disable|restore",
600
+ );
554
601
  }
555
602
 
556
603
  async function runTemplate(ctx: EmailCliContext) {