@dench.com/cli 2.2.0 → 2.2.2
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 +128 -1
- package/email.ts +356 -8
- package/lib/api-schemas.ts +2903 -62
- package/lib/command-registry.ts +319 -0
- package/package.json +1 -1
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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import type { ConvexHttpClient } from "convex/browser";
|
|
3
3
|
import { makeFunctionReference } from "convex/server";
|
|
4
|
-
import { getFlag, parseJson } from "./lib/cli-args";
|
|
4
|
+
import { getFlag, hasFlag, parseJson } from "./lib/cli-args";
|
|
5
5
|
|
|
6
6
|
type EmailCliContext = {
|
|
7
7
|
convex: ConvexHttpClient;
|
|
@@ -194,17 +194,37 @@ 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]
|
|
200
202
|
dench email template create --name "Intro" --subject "Hi {{firstName}}" --html-file body.html [--text-file body.txt] [--json]
|
|
201
203
|
dench email template preview --template <id> --data '{"firstName":"Ada"}' [--json]
|
|
202
204
|
dench email campaign create --name "Q2 outreach" --identity <identityId|fromEmail> --template <id> [--json]
|
|
205
|
+
dench email campaign list [--status sending] [--limit 25] [--json]
|
|
203
206
|
dench email campaign recipients add --campaign <id> --file recipients.jsonl [--json]
|
|
207
|
+
dench email campaign recipients list --campaign <id> [--status queued] [--limit 50] [--json]
|
|
208
|
+
dench email campaign recipients add-from-object --campaign <id> --object prospect --email-field "Email" --entries id1,id2 [--json]
|
|
204
209
|
dench email campaign preview --campaign <id> [--sample 5] [--json]
|
|
205
210
|
dench email campaign submit --campaign <id> [--json]
|
|
206
211
|
dench email campaign status --campaign <id> [--json]
|
|
207
|
-
dench email campaign pause|resume|cancel --campaign <id> [--json]
|
|
212
|
+
dench email campaign pause|resume|cancel --campaign <id> [--json]
|
|
213
|
+
|
|
214
|
+
Sequences (automatic multi-step follow-ups):
|
|
215
|
+
dench email sequence create --name "New-customer follow-up" --identity <identityId|fromEmail> [--stop-on-click] [--no-stop-on-reply] [--json]
|
|
216
|
+
dench email sequence list [--all] [--json]
|
|
217
|
+
dench email sequence status --sequence <id> [--json]
|
|
218
|
+
dench email sequence steps add --sequence <id> --subject "Hi {{firstName}}" --html-file step1.html [--delay-days 0] [--delay-hours 0] [--json]
|
|
219
|
+
dench email sequence steps update --step <id> [--subject ...] [--html-file ...] [--delay-days 3] [--json]
|
|
220
|
+
dench email sequence steps remove --step <id> [--json]
|
|
221
|
+
dench email sequence steps reorder --sequence <id> --steps id1,id2,id3 [--json]
|
|
222
|
+
dench email sequence activate|deactivate --sequence <id> [--json]
|
|
223
|
+
dench email sequence enroll --sequence <id> --file recipients.jsonl [--json]
|
|
224
|
+
dench email sequence enroll-from-object --sequence <id> --object prospect --email-field "Email" --entries id1,id2 [--json]
|
|
225
|
+
dench email sequence enrollments list --sequence <id> [--status active] [--limit 50] [--json]
|
|
226
|
+
dench email sequence enrollment pause|resume|stop --enrollment <id> [--json]
|
|
227
|
+
dench email sequence archive --sequence <id> [--json]`);
|
|
208
228
|
}
|
|
209
229
|
|
|
210
230
|
export async function runEmailCommand(ctx: EmailCliContext): Promise<void> {
|
|
@@ -233,9 +253,22 @@ export async function runEmailCommand(ctx: EmailCliContext): Promise<void> {
|
|
|
233
253
|
await runCampaign(ctx);
|
|
234
254
|
return;
|
|
235
255
|
}
|
|
256
|
+
if (group === "sequence") {
|
|
257
|
+
await runSequence(ctx);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
236
260
|
throw new EmailCliError(`Unknown dench email subcommand: ${group}`);
|
|
237
261
|
}
|
|
238
262
|
|
|
263
|
+
/** Parse a comma-separated list of CRM entry ids. */
|
|
264
|
+
function parseEntryIds(value: string | undefined): string[] {
|
|
265
|
+
if (!value?.trim()) return [];
|
|
266
|
+
return value
|
|
267
|
+
.split(",")
|
|
268
|
+
.map((id) => id.trim())
|
|
269
|
+
.filter(Boolean);
|
|
270
|
+
}
|
|
271
|
+
|
|
239
272
|
/**
|
|
240
273
|
* Parse a comma-separated address list where each entry is either a bare
|
|
241
274
|
* email or `Name <email>`.
|
|
@@ -465,12 +498,39 @@ async function runIdentity(ctx: EmailCliContext) {
|
|
|
465
498
|
(identity) =>
|
|
466
499
|
`${identity.identityId} ${identity.fromEmail} type=${identity.type}` +
|
|
467
500
|
`${identity.domain ? ` domain=${identity.domain}` : ""}` +
|
|
468
|
-
` delivery=${identity.deliveryVerificationStatus} workspace=${identity.denchVerificationStatus}
|
|
501
|
+
` delivery=${identity.deliveryVerificationStatus} workspace=${identity.denchVerificationStatus}` +
|
|
502
|
+
`${identity.disabled ? " DISABLED" : ""}`,
|
|
469
503
|
)
|
|
470
504
|
.join("\n");
|
|
471
505
|
out(ctx, { ok: true, identities }, text);
|
|
472
506
|
return;
|
|
473
507
|
}
|
|
508
|
+
if (sub === "disable" || sub === "restore") {
|
|
509
|
+
const identity = requiredFlag(ctx.args, "--identity");
|
|
510
|
+
const payload = (await callMutation(
|
|
511
|
+
ctx,
|
|
512
|
+
sub === "disable"
|
|
513
|
+
? "functions/emailCampaigns:disableIdentity"
|
|
514
|
+
: "functions/emailCampaigns:restoreIdentity",
|
|
515
|
+
{ identity },
|
|
516
|
+
)) as JsonRecord;
|
|
517
|
+
const rows = (
|
|
518
|
+
Array.isArray(payload.disabled)
|
|
519
|
+
? payload.disabled
|
|
520
|
+
: Array.isArray(payload.restored)
|
|
521
|
+
? payload.restored
|
|
522
|
+
: []
|
|
523
|
+
) as JsonRecord[];
|
|
524
|
+
const verb = sub === "disable" ? "Disabled" : "Restored";
|
|
525
|
+
const text =
|
|
526
|
+
rows.length === 0
|
|
527
|
+
? `${verb} 0 identities.`
|
|
528
|
+
: `${verb} ${rows.length} sender(s):\n${rows
|
|
529
|
+
.map((row) => ` ${row.fromEmail} (${row.identityId})`)
|
|
530
|
+
.join("\n")}`;
|
|
531
|
+
out(ctx, payload, text);
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
474
534
|
if (sub === "dns") {
|
|
475
535
|
const identity = requiredFlag(ctx.args, "--identity");
|
|
476
536
|
if (!identity.includes("@") && !identity.includes(".")) {
|
|
@@ -509,12 +569,19 @@ async function runIdentity(ctx: EmailCliContext) {
|
|
|
509
569
|
: [];
|
|
510
570
|
const records = buildDnsRecords(domain, tokens);
|
|
511
571
|
const verified = status.verifiedForSendingStatus === true;
|
|
512
|
-
const payload = {
|
|
572
|
+
const payload: JsonRecord = {
|
|
513
573
|
ok: true,
|
|
514
574
|
domain,
|
|
515
575
|
domainVerified: verified,
|
|
516
576
|
dnsRecords: records,
|
|
517
577
|
};
|
|
578
|
+
if (
|
|
579
|
+
!verified &&
|
|
580
|
+
status.verificationInfo &&
|
|
581
|
+
typeof status.verificationInfo === "object"
|
|
582
|
+
) {
|
|
583
|
+
payload.verificationInfo = status.verificationInfo;
|
|
584
|
+
}
|
|
518
585
|
const text = verified
|
|
519
586
|
? `Domain ${domain} is verified — DNS records below are already in place.\n${formatDnsRecords(records)}`
|
|
520
587
|
: tokens.length > 0
|
|
@@ -541,16 +608,27 @@ async function runIdentity(ctx: EmailCliContext) {
|
|
|
541
608
|
`/v1/email/identities/${encodeURIComponent(identity)}/status`,
|
|
542
609
|
);
|
|
543
610
|
const status = payload as JsonRecord;
|
|
544
|
-
const shaped = {
|
|
611
|
+
const shaped: JsonRecord = {
|
|
545
612
|
ok: status.ok === true,
|
|
546
613
|
identity: status.identity,
|
|
547
614
|
deliveryVerified: status.verifiedForSendingStatus === true,
|
|
548
615
|
deliveryVerificationStatus: status.verificationStatus,
|
|
549
616
|
};
|
|
617
|
+
// Pending domains: surface when the provider last polled DNS and
|
|
618
|
+
// why it failed, so callers know whether to fix records or wait.
|
|
619
|
+
if (
|
|
620
|
+
status.verifiedForSendingStatus !== true &&
|
|
621
|
+
status.verificationInfo &&
|
|
622
|
+
typeof status.verificationInfo === "object"
|
|
623
|
+
) {
|
|
624
|
+
shaped.verificationInfo = status.verificationInfo;
|
|
625
|
+
}
|
|
550
626
|
out(ctx, shaped, JSON.stringify(shaped, null, 2));
|
|
551
627
|
return;
|
|
552
628
|
}
|
|
553
|
-
throw new EmailCliError(
|
|
629
|
+
throw new EmailCliError(
|
|
630
|
+
"Usage: dench email identity verify|list|dns|status|disable|restore",
|
|
631
|
+
);
|
|
554
632
|
}
|
|
555
633
|
|
|
556
634
|
async function runTemplate(ctx: EmailCliContext) {
|
|
@@ -615,11 +693,60 @@ async function runCampaign(ctx: EmailCliContext) {
|
|
|
615
693
|
);
|
|
616
694
|
return;
|
|
617
695
|
}
|
|
696
|
+
if (sub === "list") {
|
|
697
|
+
const limitRaw = getFlag(ctx.args, "--limit");
|
|
698
|
+
const status = getFlag(ctx.args, "--status");
|
|
699
|
+
const payload = await callQuery(
|
|
700
|
+
ctx,
|
|
701
|
+
"functions/emailCampaigns:listCampaigns",
|
|
702
|
+
{
|
|
703
|
+
paginationOpts: {
|
|
704
|
+
numItems: limitRaw ? Number(limitRaw) : 25,
|
|
705
|
+
cursor: null,
|
|
706
|
+
},
|
|
707
|
+
status,
|
|
708
|
+
},
|
|
709
|
+
);
|
|
710
|
+
out(ctx, payload, JSON.stringify(payload, null, 2));
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
618
713
|
if (sub === "recipients") {
|
|
619
714
|
const nested = ctx.args.shift();
|
|
715
|
+
if (nested === "list") {
|
|
716
|
+
const limitRaw = getFlag(ctx.args, "--limit");
|
|
717
|
+
const status = getFlag(ctx.args, "--status");
|
|
718
|
+
const payload = await callQuery(
|
|
719
|
+
ctx,
|
|
720
|
+
"functions/emailCampaigns:listRecipients",
|
|
721
|
+
{
|
|
722
|
+
campaignId: requiredFlag(ctx.args, "--campaign"),
|
|
723
|
+
paginationOpts: {
|
|
724
|
+
numItems: limitRaw ? Number(limitRaw) : 50,
|
|
725
|
+
cursor: null,
|
|
726
|
+
},
|
|
727
|
+
status,
|
|
728
|
+
},
|
|
729
|
+
);
|
|
730
|
+
out(ctx, payload, JSON.stringify(payload, null, 2));
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
if (nested === "add-from-object") {
|
|
734
|
+
const payload = await callMutation(
|
|
735
|
+
ctx,
|
|
736
|
+
"functions/emailCampaigns:addRecipientsFromCrm",
|
|
737
|
+
{
|
|
738
|
+
campaignId: requiredFlag(ctx.args, "--campaign"),
|
|
739
|
+
objectName: requiredFlag(ctx.args, "--object"),
|
|
740
|
+
emailFieldName: getFlag(ctx.args, "--email-field"),
|
|
741
|
+
entryIds: parseEntryIds(requiredFlag(ctx.args, "--entries")),
|
|
742
|
+
},
|
|
743
|
+
);
|
|
744
|
+
out(ctx, payload, JSON.stringify(payload, null, 2));
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
620
747
|
if (nested !== "add") {
|
|
621
748
|
throw new EmailCliError(
|
|
622
|
-
"Usage: dench email campaign recipients add
|
|
749
|
+
"Usage: dench email campaign recipients add|list|add-from-object",
|
|
623
750
|
);
|
|
624
751
|
}
|
|
625
752
|
const campaignId = requiredFlag(ctx.args, "--campaign");
|
|
@@ -687,6 +814,227 @@ async function runCampaign(ctx: EmailCliContext) {
|
|
|
687
814
|
return;
|
|
688
815
|
}
|
|
689
816
|
throw new EmailCliError(
|
|
690
|
-
"Usage: dench email campaign create|recipients|preview|submit|status|pause|resume|cancel",
|
|
817
|
+
"Usage: dench email campaign create|list|recipients|preview|submit|status|pause|resume|cancel",
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
async function runSequence(ctx: EmailCliContext) {
|
|
822
|
+
const sub = ctx.args.shift();
|
|
823
|
+
if (sub === "create") {
|
|
824
|
+
const stopOnReply = !hasFlag(ctx.args, "--no-stop-on-reply");
|
|
825
|
+
const payload = await callMutation(
|
|
826
|
+
ctx,
|
|
827
|
+
"functions/emailSequences:createSequence",
|
|
828
|
+
{
|
|
829
|
+
name: requiredFlag(ctx.args, "--name"),
|
|
830
|
+
identityId: requiredFlag(ctx.args, "--identity"),
|
|
831
|
+
replyToMode: getFlag(ctx.args, "--reply-to-mode"),
|
|
832
|
+
stopOnReply,
|
|
833
|
+
stopOnClick: getFlag(ctx.args, "--stop-on-click") !== undefined,
|
|
834
|
+
},
|
|
835
|
+
);
|
|
836
|
+
out(
|
|
837
|
+
ctx,
|
|
838
|
+
payload,
|
|
839
|
+
`Sequence created: ${(payload as JsonRecord).sequenceId}`,
|
|
840
|
+
);
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
if (sub === "update") {
|
|
844
|
+
const payload = await callMutation(
|
|
845
|
+
ctx,
|
|
846
|
+
"functions/emailSequences:updateSequence",
|
|
847
|
+
{
|
|
848
|
+
sequenceId: requiredFlag(ctx.args, "--sequence"),
|
|
849
|
+
name: getFlag(ctx.args, "--name"),
|
|
850
|
+
identityId: getFlag(ctx.args, "--identity"),
|
|
851
|
+
replyToMode: getFlag(ctx.args, "--reply-to-mode"),
|
|
852
|
+
},
|
|
853
|
+
);
|
|
854
|
+
out(ctx, payload, JSON.stringify(payload, null, 2));
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
if (sub === "activate" || sub === "deactivate") {
|
|
858
|
+
const payload = await callMutation(
|
|
859
|
+
ctx,
|
|
860
|
+
"functions/emailSequences:setSequenceActive",
|
|
861
|
+
{
|
|
862
|
+
sequenceId: requiredFlag(ctx.args, "--sequence"),
|
|
863
|
+
active: sub === "activate",
|
|
864
|
+
},
|
|
865
|
+
);
|
|
866
|
+
out(ctx, payload, JSON.stringify(payload, null, 2));
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
if (sub === "archive") {
|
|
870
|
+
const payload = await callMutation(
|
|
871
|
+
ctx,
|
|
872
|
+
"functions/emailSequences:archiveSequence",
|
|
873
|
+
{ sequenceId: requiredFlag(ctx.args, "--sequence") },
|
|
874
|
+
);
|
|
875
|
+
out(ctx, payload, JSON.stringify(payload, null, 2));
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
if (sub === "list") {
|
|
879
|
+
const payload = await callQuery(
|
|
880
|
+
ctx,
|
|
881
|
+
"functions/emailSequences:listSequences",
|
|
882
|
+
{ includeArchived: getFlag(ctx.args, "--all") !== undefined },
|
|
883
|
+
);
|
|
884
|
+
out(ctx, payload, JSON.stringify(payload, null, 2));
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
if (sub === "status") {
|
|
888
|
+
const payload = await callQuery(
|
|
889
|
+
ctx,
|
|
890
|
+
"functions/emailSequences:getSequence",
|
|
891
|
+
{ sequenceId: requiredFlag(ctx.args, "--sequence") },
|
|
892
|
+
);
|
|
893
|
+
out(ctx, payload, JSON.stringify(payload, null, 2));
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
if (sub === "steps") {
|
|
897
|
+
const nested = ctx.args.shift();
|
|
898
|
+
if (nested === "add") {
|
|
899
|
+
const htmlBody = await readTextFlag(ctx.args, "--html", "--html-file");
|
|
900
|
+
if (!htmlBody) throw new EmailCliError("Missing --html or --html-file");
|
|
901
|
+
const delayDaysRaw = getFlag(ctx.args, "--delay-days");
|
|
902
|
+
const delayHoursRaw = getFlag(ctx.args, "--delay-hours");
|
|
903
|
+
const delayHours =
|
|
904
|
+
(delayDaysRaw ? Number(delayDaysRaw) * 24 : 0) +
|
|
905
|
+
(delayHoursRaw ? Number(delayHoursRaw) : 0);
|
|
906
|
+
const payload = await callMutation(
|
|
907
|
+
ctx,
|
|
908
|
+
"functions/emailSequences:addStep",
|
|
909
|
+
{
|
|
910
|
+
sequenceId: requiredFlag(ctx.args, "--sequence"),
|
|
911
|
+
subject: requiredFlag(ctx.args, "--subject"),
|
|
912
|
+
htmlBody,
|
|
913
|
+
textBody: await readTextFlag(ctx.args, "--text", "--text-file"),
|
|
914
|
+
delayHours,
|
|
915
|
+
},
|
|
916
|
+
);
|
|
917
|
+
out(ctx, payload, `Step added: ${(payload as JsonRecord).stepId}`);
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
if (nested === "update") {
|
|
921
|
+
const delayDaysRaw = getFlag(ctx.args, "--delay-days");
|
|
922
|
+
const delayHoursRaw = getFlag(ctx.args, "--delay-hours");
|
|
923
|
+
const delayHours =
|
|
924
|
+
delayDaysRaw || delayHoursRaw
|
|
925
|
+
? (delayDaysRaw ? Number(delayDaysRaw) * 24 : 0) +
|
|
926
|
+
(delayHoursRaw ? Number(delayHoursRaw) : 0)
|
|
927
|
+
: undefined;
|
|
928
|
+
const payload = await callMutation(
|
|
929
|
+
ctx,
|
|
930
|
+
"functions/emailSequences:updateStep",
|
|
931
|
+
{
|
|
932
|
+
stepId: requiredFlag(ctx.args, "--step"),
|
|
933
|
+
subject: getFlag(ctx.args, "--subject"),
|
|
934
|
+
htmlBody: await readTextFlag(ctx.args, "--html", "--html-file"),
|
|
935
|
+
textBody: await readTextFlag(ctx.args, "--text", "--text-file"),
|
|
936
|
+
delayHours,
|
|
937
|
+
},
|
|
938
|
+
);
|
|
939
|
+
out(ctx, payload, JSON.stringify(payload, null, 2));
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
if (nested === "remove") {
|
|
943
|
+
const payload = await callMutation(
|
|
944
|
+
ctx,
|
|
945
|
+
"functions/emailSequences:removeStep",
|
|
946
|
+
{ stepId: requiredFlag(ctx.args, "--step") },
|
|
947
|
+
);
|
|
948
|
+
out(ctx, payload, JSON.stringify(payload, null, 2));
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
if (nested === "reorder") {
|
|
952
|
+
const payload = await callMutation(
|
|
953
|
+
ctx,
|
|
954
|
+
"functions/emailSequences:reorderSteps",
|
|
955
|
+
{
|
|
956
|
+
sequenceId: requiredFlag(ctx.args, "--sequence"),
|
|
957
|
+
orderedStepIds: parseEntryIds(requiredFlag(ctx.args, "--steps")),
|
|
958
|
+
},
|
|
959
|
+
);
|
|
960
|
+
out(ctx, payload, JSON.stringify(payload, null, 2));
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
throw new EmailCliError(
|
|
964
|
+
"Usage: dench email sequence steps add|update|remove|reorder",
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
if (sub === "enroll") {
|
|
968
|
+
const file = getFlag(ctx.args, "--file");
|
|
969
|
+
const rows = file
|
|
970
|
+
? await readRecipients(file)
|
|
971
|
+
: ((optionalJsonFlag(ctx.args, "--data") as JsonRecord[] | undefined) ??
|
|
972
|
+
[]);
|
|
973
|
+
if (rows.length === 0) throw new EmailCliError("No recipients provided");
|
|
974
|
+
const payload = await callMutation(ctx, "functions/emailSequences:enroll", {
|
|
975
|
+
sequenceId: requiredFlag(ctx.args, "--sequence"),
|
|
976
|
+
recipients: toRecipients(rows),
|
|
977
|
+
});
|
|
978
|
+
out(ctx, payload, JSON.stringify(payload, null, 2));
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
if (sub === "enroll-from-object") {
|
|
982
|
+
const payload = await callMutation(
|
|
983
|
+
ctx,
|
|
984
|
+
"functions/emailSequences:enrollFromCrm",
|
|
985
|
+
{
|
|
986
|
+
sequenceId: requiredFlag(ctx.args, "--sequence"),
|
|
987
|
+
objectName: requiredFlag(ctx.args, "--object"),
|
|
988
|
+
emailFieldName: getFlag(ctx.args, "--email-field"),
|
|
989
|
+
entryIds: parseEntryIds(requiredFlag(ctx.args, "--entries")),
|
|
990
|
+
},
|
|
991
|
+
);
|
|
992
|
+
out(ctx, payload, JSON.stringify(payload, null, 2));
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
if (sub === "enrollments") {
|
|
996
|
+
const nested = ctx.args.shift();
|
|
997
|
+
if (nested !== "list") {
|
|
998
|
+
throw new EmailCliError("Usage: dench email sequence enrollments list");
|
|
999
|
+
}
|
|
1000
|
+
const limitRaw = getFlag(ctx.args, "--limit");
|
|
1001
|
+
const payload = await callQuery(
|
|
1002
|
+
ctx,
|
|
1003
|
+
"functions/emailSequences:listEnrollments",
|
|
1004
|
+
{
|
|
1005
|
+
sequenceId: requiredFlag(ctx.args, "--sequence"),
|
|
1006
|
+
paginationOpts: {
|
|
1007
|
+
numItems: limitRaw ? Number(limitRaw) : 50,
|
|
1008
|
+
cursor: null,
|
|
1009
|
+
},
|
|
1010
|
+
status: getFlag(ctx.args, "--status"),
|
|
1011
|
+
},
|
|
1012
|
+
);
|
|
1013
|
+
out(ctx, payload, JSON.stringify(payload, null, 2));
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
if (sub === "enrollment") {
|
|
1017
|
+
const nested = ctx.args.shift();
|
|
1018
|
+
const fn =
|
|
1019
|
+
nested === "pause"
|
|
1020
|
+
? "pauseEnrollment"
|
|
1021
|
+
: nested === "resume"
|
|
1022
|
+
? "resumeEnrollment"
|
|
1023
|
+
: nested === "stop"
|
|
1024
|
+
? "stopEnrollment"
|
|
1025
|
+
: null;
|
|
1026
|
+
if (!fn) {
|
|
1027
|
+
throw new EmailCliError(
|
|
1028
|
+
"Usage: dench email sequence enrollment pause|resume|stop --enrollment <id>",
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
const payload = await callMutation(ctx, `functions/emailSequences:${fn}`, {
|
|
1032
|
+
enrollmentId: requiredFlag(ctx.args, "--enrollment"),
|
|
1033
|
+
});
|
|
1034
|
+
out(ctx, payload, JSON.stringify(payload, null, 2));
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
throw new EmailCliError(
|
|
1038
|
+
"Usage: dench email sequence create|update|activate|deactivate|archive|list|status|steps|enroll|enroll-from-object|enrollments|enrollment",
|
|
691
1039
|
);
|
|
692
1040
|
}
|