@hogsend/cli 0.2.3 → 0.6.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.
- package/dist/bin.js +904 -168
- package/dist/bin.js.map +1 -1
- package/package.json +1 -1
- package/skills/hogsend-authoring-journeys/SKILL.md +1 -1
- package/skills/hogsend-authoring-journeys/references/journey-meta.md +8 -3
- package/skills/hogsend-authoring-lists/SKILL.md +178 -0
- package/skills/hogsend-cli/SKILL.md +44 -18
- package/skills/hogsend-client-sdk/SKILL.md +185 -0
- package/skills/hogsend-client-sdk/references/api-surface.md +181 -0
- package/src/bin.ts +4 -1
- package/src/commands/campaigns.ts +309 -0
- package/src/commands/contacts.ts +176 -6
- package/src/commands/emails.ts +231 -0
- package/src/commands/events.ts +253 -15
- package/src/commands/index.ts +4 -0
- package/src/commands/types.ts +8 -2
- package/src/lib/config.ts +23 -1
- package/src/lib/http.ts +122 -49
- package/studio/assets/{index-r9qr4mus.js → index-D7Ax_oFF.js} +1 -1
- package/studio/index.html +1 -1
package/src/commands/contacts.ts
CHANGED
|
@@ -5,12 +5,14 @@ import type { Command, CommandContext } from "./types.js";
|
|
|
5
5
|
|
|
6
6
|
const usage = `hogsend contacts <subcommand> [options]
|
|
7
7
|
|
|
8
|
-
Inspect contacts via the
|
|
8
|
+
Inspect contacts via the admin API (/v1/admin/contacts) and upsert them via the
|
|
9
|
+
data plane (PUT /v1/contacts).
|
|
9
10
|
|
|
10
11
|
Subcommands:
|
|
11
12
|
list List contacts (newest activity first).
|
|
12
13
|
get <id> Get one contact (by id or externalId) + preferences.
|
|
13
14
|
timeline <id> Merged event/email/journey activity for a contact.
|
|
15
|
+
upsert Create or update a contact (PUT /v1/contacts).
|
|
14
16
|
|
|
15
17
|
list options:
|
|
16
18
|
--search <q> Filter by email/externalId substring.
|
|
@@ -22,12 +24,24 @@ timeline options:
|
|
|
22
24
|
--limit <n> Page size (1-100, default 50).
|
|
23
25
|
--offset <n> Page offset (default 0).
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
upsert options (at least one of --email / --user-id required):
|
|
28
|
+
--email <addr> Contact email (a resolvable identity key).
|
|
29
|
+
--user-id <id> External (distinct) id.
|
|
30
|
+
--prop <key=value> Contact property; repeatable. Value parsed as JSON,
|
|
31
|
+
falling back to a string. Uses the data plane (ingest key).
|
|
32
|
+
--props <json> Contact properties as one JSON object (merged with --prop).
|
|
33
|
+
--list <id> Subscribe to a list; repeatable.
|
|
34
|
+
--unlist <id> Unsubscribe from a list; repeatable.
|
|
35
|
+
|
|
36
|
+
Global options (handled by the router): --url, --admin-key, --data-key, --json,
|
|
37
|
+
-h/--help.
|
|
26
38
|
|
|
27
39
|
Examples:
|
|
28
40
|
hogsend contacts list --search acme@ --json
|
|
29
41
|
hogsend contacts get user_123
|
|
30
|
-
hogsend contacts timeline user_123 --type email --json
|
|
42
|
+
hogsend contacts timeline user_123 --type email --json
|
|
43
|
+
hogsend contacts upsert --email a@b.com --user-id user_123 --prop plan=pro
|
|
44
|
+
hogsend contacts upsert --user-id user_123 --props '{"plan":"pro","seats":5}'`;
|
|
31
45
|
|
|
32
46
|
type ContactRecord = {
|
|
33
47
|
id: string;
|
|
@@ -75,8 +89,94 @@ type TimelineResponse = {
|
|
|
75
89
|
offset: number;
|
|
76
90
|
};
|
|
77
91
|
|
|
92
|
+
/** Shape returned by PUT /v1/contacts. */
|
|
93
|
+
type UpsertResponse = {
|
|
94
|
+
id: string;
|
|
95
|
+
created: boolean;
|
|
96
|
+
linked: boolean;
|
|
97
|
+
};
|
|
98
|
+
|
|
78
99
|
const badge = `${color.bgMagenta(color.black(" hogsend "))} contacts`;
|
|
79
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Parse `--prop key=value` (repeatable) + an optional `--props <json>` object
|
|
103
|
+
* into a single properties record. Each `--prop` value is JSON-parsed when it
|
|
104
|
+
* is valid JSON (numbers/booleans/null/objects), else kept as a string. The
|
|
105
|
+
* explicit `--props` object is applied first, so later `--prop` flags win.
|
|
106
|
+
*/
|
|
107
|
+
function parseProps(
|
|
108
|
+
ctx: CommandContext,
|
|
109
|
+
propsJson: string | undefined,
|
|
110
|
+
propPairs: string[] | undefined,
|
|
111
|
+
): Record<string, unknown> | undefined {
|
|
112
|
+
const out: Record<string, unknown> = {};
|
|
113
|
+
let any = false;
|
|
114
|
+
|
|
115
|
+
if (propsJson !== undefined) {
|
|
116
|
+
let parsed: unknown;
|
|
117
|
+
try {
|
|
118
|
+
parsed = JSON.parse(propsJson);
|
|
119
|
+
} catch {
|
|
120
|
+
ctx.out.fail(`--props must be valid JSON, got: ${propsJson}`);
|
|
121
|
+
}
|
|
122
|
+
if (
|
|
123
|
+
parsed === null ||
|
|
124
|
+
typeof parsed !== "object" ||
|
|
125
|
+
Array.isArray(parsed)
|
|
126
|
+
) {
|
|
127
|
+
ctx.out.fail("--props must be a JSON object");
|
|
128
|
+
}
|
|
129
|
+
Object.assign(out, parsed as Record<string, unknown>);
|
|
130
|
+
any = true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
for (const pair of propPairs ?? []) {
|
|
134
|
+
const eq = pair.indexOf("=");
|
|
135
|
+
if (eq === -1) {
|
|
136
|
+
ctx.out.fail(`--prop must be key=value, got: ${pair}`);
|
|
137
|
+
}
|
|
138
|
+
const key = pair.slice(0, eq).trim();
|
|
139
|
+
if (key === "") {
|
|
140
|
+
ctx.out.fail(`--prop key cannot be empty, got: ${pair}`);
|
|
141
|
+
}
|
|
142
|
+
const raw = pair.slice(eq + 1);
|
|
143
|
+
out[key] = coerceValue(raw);
|
|
144
|
+
any = true;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return any ? out : undefined;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** JSON-parse a flag value, falling back to the raw string. */
|
|
151
|
+
function coerceValue(raw: string): unknown {
|
|
152
|
+
try {
|
|
153
|
+
return JSON.parse(raw);
|
|
154
|
+
} catch {
|
|
155
|
+
return raw;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Build a `lists` map from repeatable `--list <id>` (true) / `--unlist <id>`
|
|
161
|
+
* (false) flags. Returns undefined when neither was passed.
|
|
162
|
+
*/
|
|
163
|
+
function parseLists(
|
|
164
|
+
subscribe: string[] | undefined,
|
|
165
|
+
unsubscribe: string[] | undefined,
|
|
166
|
+
): Record<string, boolean> | undefined {
|
|
167
|
+
const out: Record<string, boolean> = {};
|
|
168
|
+
let any = false;
|
|
169
|
+
for (const id of subscribe ?? []) {
|
|
170
|
+
out[id] = true;
|
|
171
|
+
any = true;
|
|
172
|
+
}
|
|
173
|
+
for (const id of unsubscribe ?? []) {
|
|
174
|
+
out[id] = false;
|
|
175
|
+
any = true;
|
|
176
|
+
}
|
|
177
|
+
return any ? out : undefined;
|
|
178
|
+
}
|
|
179
|
+
|
|
80
180
|
/** Run an HTTP call, mapping HttpError into a clean ctx.out.fail message. */
|
|
81
181
|
async function fetchOrFail<T>(
|
|
82
182
|
ctx: CommandContext,
|
|
@@ -286,6 +386,73 @@ function summarizeTimelineEntry(entry: TimelineEntry): string {
|
|
|
286
386
|
return `${subject} [${String(d.status ?? "")}]`;
|
|
287
387
|
}
|
|
288
388
|
|
|
389
|
+
async function runUpsert(ctx: CommandContext, argv: string[]): Promise<void> {
|
|
390
|
+
const { values } = parseArgs({
|
|
391
|
+
args: argv,
|
|
392
|
+
allowPositionals: true,
|
|
393
|
+
options: {
|
|
394
|
+
email: { type: "string" },
|
|
395
|
+
"user-id": { type: "string" },
|
|
396
|
+
prop: { type: "string", multiple: true },
|
|
397
|
+
props: { type: "string" },
|
|
398
|
+
list: { type: "string", multiple: true },
|
|
399
|
+
unlist: { type: "string", multiple: true },
|
|
400
|
+
help: { type: "boolean", short: "h", default: false },
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
if (values.help) {
|
|
405
|
+
ctx.out.log(usage);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const email = values.email;
|
|
410
|
+
const userId = values["user-id"];
|
|
411
|
+
if (!email && !userId) {
|
|
412
|
+
ctx.out.fail(
|
|
413
|
+
"contacts upsert requires at least one of --email or --user-id",
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const properties = parseProps(ctx, values.props, values.prop);
|
|
418
|
+
const lists = parseLists(values.list, values.unlist);
|
|
419
|
+
|
|
420
|
+
const body: {
|
|
421
|
+
email?: string;
|
|
422
|
+
userId?: string;
|
|
423
|
+
properties?: Record<string, unknown>;
|
|
424
|
+
lists?: Record<string, boolean>;
|
|
425
|
+
} = {};
|
|
426
|
+
if (email) body.email = email;
|
|
427
|
+
if (userId) body.userId = userId;
|
|
428
|
+
if (properties) body.properties = properties;
|
|
429
|
+
if (lists) body.lists = lists;
|
|
430
|
+
|
|
431
|
+
if (!ctx.json) ctx.out.intro(`${badge} upsert`);
|
|
432
|
+
|
|
433
|
+
const res = await fetchOrFail<UpsertResponse>(ctx, "Upserting contact", () =>
|
|
434
|
+
ctx.dataHttp.put<UpsertResponse>("/v1/contacts", body),
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
if (ctx.json) {
|
|
438
|
+
ctx.out.json(res);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
ctx.out.kv(
|
|
443
|
+
{
|
|
444
|
+
id: res.id,
|
|
445
|
+
created: res.created,
|
|
446
|
+
linked: res.linked,
|
|
447
|
+
email: email ?? color.dim("(none)"),
|
|
448
|
+
userId: userId ?? color.dim("(none)"),
|
|
449
|
+
},
|
|
450
|
+
"Contact",
|
|
451
|
+
);
|
|
452
|
+
const verb = res.created ? "created" : "updated";
|
|
453
|
+
ctx.out.outro(`Contact ${color.cyan(res.id)} ${verb}.`);
|
|
454
|
+
}
|
|
455
|
+
|
|
289
456
|
async function run(ctx: CommandContext): Promise<void> {
|
|
290
457
|
const sub = ctx.argv[0];
|
|
291
458
|
|
|
@@ -296,21 +463,24 @@ async function run(ctx: CommandContext): Promise<void> {
|
|
|
296
463
|
return runGet(ctx, ctx.argv);
|
|
297
464
|
case "timeline":
|
|
298
465
|
return runTimeline(ctx, ctx.argv);
|
|
466
|
+
case "upsert":
|
|
467
|
+
// Strip the leading "upsert" token; the rest is upsert's own flags.
|
|
468
|
+
return runUpsert(ctx, ctx.argv.slice(1));
|
|
299
469
|
case undefined:
|
|
300
470
|
ctx.out.fail(
|
|
301
|
-
"contacts requires a subcommand: list, get, or
|
|
471
|
+
"contacts requires a subcommand: list, get, timeline, or upsert (see hogsend contacts --help)",
|
|
302
472
|
);
|
|
303
473
|
break;
|
|
304
474
|
default:
|
|
305
475
|
ctx.out.fail(
|
|
306
|
-
`unknown contacts subcommand "${sub}" — expected list, get, or
|
|
476
|
+
`unknown contacts subcommand "${sub}" — expected list, get, timeline, or upsert`,
|
|
307
477
|
);
|
|
308
478
|
}
|
|
309
479
|
}
|
|
310
480
|
|
|
311
481
|
export const contactsCommand: Command = {
|
|
312
482
|
name: "contacts",
|
|
313
|
-
summary: "List, inspect, and
|
|
483
|
+
summary: "List, inspect, trace, and upsert contacts",
|
|
314
484
|
usage,
|
|
315
485
|
run,
|
|
316
486
|
};
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { isHttpError } from "../lib/http.js";
|
|
3
|
+
import { color } from "../lib/output.js";
|
|
4
|
+
import type { Command, CommandContext } from "./types.js";
|
|
5
|
+
|
|
6
|
+
const usage = `hogsend emails <subcommand> [options]
|
|
7
|
+
|
|
8
|
+
Send a transactional email through the data plane (POST /v1/emails). The send
|
|
9
|
+
runs through the full preferences + tracking pipeline (link-click + open).
|
|
10
|
+
|
|
11
|
+
Subcommands:
|
|
12
|
+
send <template> Send the named template to a recipient.
|
|
13
|
+
|
|
14
|
+
send options (at least one of --to / --user-id required):
|
|
15
|
+
--to <addr> Recipient email address.
|
|
16
|
+
--user-id <id> External (distinct) id; the recipient email is resolved
|
|
17
|
+
from the contact (404 if it has no resolvable email).
|
|
18
|
+
--prop <key=value> Template prop; repeatable. Value parsed as JSON, falling
|
|
19
|
+
back to a string.
|
|
20
|
+
--props <json> Template props as one JSON object (merged with --prop).
|
|
21
|
+
--from <addr> Override the default From address.
|
|
22
|
+
--subject <text> Override the rendered subject.
|
|
23
|
+
--reply-to <addr> Set the Reply-To address.
|
|
24
|
+
--category <key> Preference category / list id to gate the send on.
|
|
25
|
+
--skip-preference-check Bypass unsubscribe/suppression (requires full-admin).
|
|
26
|
+
--idempotency-key <k> Dedup key.
|
|
27
|
+
|
|
28
|
+
Global options (handled by the router): --url, --admin-key, --data-key, --json,
|
|
29
|
+
-h/--help.
|
|
30
|
+
|
|
31
|
+
Examples:
|
|
32
|
+
hogsend emails send welcome --to a@b.com --prop name=Ada
|
|
33
|
+
hogsend emails send welcome --user-id user_123 --props '{"name":"Ada"}' --json`;
|
|
34
|
+
|
|
35
|
+
/** Shape returned by POST /v1/emails. */
|
|
36
|
+
interface SendResponse {
|
|
37
|
+
emailSendId: string;
|
|
38
|
+
status: "queued" | "sent" | "suppressed" | "unsubscribed" | "skipped";
|
|
39
|
+
reason?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const badge = `${color.bgMagenta(color.black(" hogsend "))} emails`;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Parse `--prop key=value` (repeatable) + an optional `--props <json>` object
|
|
46
|
+
* into a single props record. Each `--prop` value is JSON-parsed when valid
|
|
47
|
+
* JSON, else kept as a string. The `--props` object is applied first, so later
|
|
48
|
+
* `--prop` flags win.
|
|
49
|
+
*/
|
|
50
|
+
function parseProps(
|
|
51
|
+
ctx: CommandContext,
|
|
52
|
+
propsJson: string | undefined,
|
|
53
|
+
propPairs: string[] | undefined,
|
|
54
|
+
): Record<string, unknown> | undefined {
|
|
55
|
+
const out: Record<string, unknown> = {};
|
|
56
|
+
let any = false;
|
|
57
|
+
|
|
58
|
+
if (propsJson !== undefined) {
|
|
59
|
+
let parsed: unknown;
|
|
60
|
+
try {
|
|
61
|
+
parsed = JSON.parse(propsJson);
|
|
62
|
+
} catch {
|
|
63
|
+
ctx.out.fail(`--props must be valid JSON, got: ${propsJson}`);
|
|
64
|
+
}
|
|
65
|
+
if (
|
|
66
|
+
parsed === null ||
|
|
67
|
+
typeof parsed !== "object" ||
|
|
68
|
+
Array.isArray(parsed)
|
|
69
|
+
) {
|
|
70
|
+
ctx.out.fail("--props must be a JSON object");
|
|
71
|
+
}
|
|
72
|
+
Object.assign(out, parsed as Record<string, unknown>);
|
|
73
|
+
any = true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
for (const pair of propPairs ?? []) {
|
|
77
|
+
const eq = pair.indexOf("=");
|
|
78
|
+
if (eq === -1) {
|
|
79
|
+
ctx.out.fail(`--prop must be key=value, got: ${pair}`);
|
|
80
|
+
}
|
|
81
|
+
const key = pair.slice(0, eq).trim();
|
|
82
|
+
if (key === "") {
|
|
83
|
+
ctx.out.fail(`--prop key cannot be empty, got: ${pair}`);
|
|
84
|
+
}
|
|
85
|
+
out[key] = coerceValue(pair.slice(eq + 1));
|
|
86
|
+
any = true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return any ? out : undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** JSON-parse a flag value, falling back to the raw string. */
|
|
93
|
+
function coerceValue(raw: string): unknown {
|
|
94
|
+
try {
|
|
95
|
+
return JSON.parse(raw);
|
|
96
|
+
} catch {
|
|
97
|
+
return raw;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function statusColor(status: SendResponse["status"]): string {
|
|
102
|
+
switch (status) {
|
|
103
|
+
case "queued":
|
|
104
|
+
case "sent":
|
|
105
|
+
return color.green(status);
|
|
106
|
+
case "skipped":
|
|
107
|
+
return color.dim(status);
|
|
108
|
+
default:
|
|
109
|
+
// suppressed | unsubscribed
|
|
110
|
+
return color.yellow(status);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function runSend(ctx: CommandContext, argv: string[]): Promise<void> {
|
|
115
|
+
const { values, positionals } = parseArgs({
|
|
116
|
+
args: argv,
|
|
117
|
+
allowPositionals: true,
|
|
118
|
+
options: {
|
|
119
|
+
to: { type: "string" },
|
|
120
|
+
"user-id": { type: "string" },
|
|
121
|
+
prop: { type: "string", multiple: true },
|
|
122
|
+
props: { type: "string" },
|
|
123
|
+
from: { type: "string" },
|
|
124
|
+
subject: { type: "string" },
|
|
125
|
+
"reply-to": { type: "string" },
|
|
126
|
+
category: { type: "string" },
|
|
127
|
+
"skip-preference-check": { type: "boolean", default: false },
|
|
128
|
+
"idempotency-key": { type: "string" },
|
|
129
|
+
help: { type: "boolean", short: "h", default: false },
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
if (values.help) {
|
|
134
|
+
ctx.out.log(usage);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// positionals[0] is the template name (the "send" token was already stripped).
|
|
139
|
+
const template = positionals[0];
|
|
140
|
+
if (!template) {
|
|
141
|
+
ctx.out.fail(
|
|
142
|
+
"emails send requires a template, e.g. hogsend emails send welcome --to a@b.com",
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const to = values.to;
|
|
147
|
+
const userId = values["user-id"];
|
|
148
|
+
if (!to && !userId) {
|
|
149
|
+
ctx.out.fail("emails send requires at least one of --to or --user-id");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const props = parseProps(ctx, values.props, values.prop);
|
|
153
|
+
|
|
154
|
+
const body: {
|
|
155
|
+
template: string;
|
|
156
|
+
to?: string;
|
|
157
|
+
userId?: string;
|
|
158
|
+
props?: Record<string, unknown>;
|
|
159
|
+
from?: string;
|
|
160
|
+
subject?: string;
|
|
161
|
+
replyTo?: string;
|
|
162
|
+
category?: string;
|
|
163
|
+
skipPreferenceCheck?: boolean;
|
|
164
|
+
idempotencyKey?: string;
|
|
165
|
+
} = { template };
|
|
166
|
+
if (to) body.to = to;
|
|
167
|
+
if (userId) body.userId = userId;
|
|
168
|
+
if (props) body.props = props;
|
|
169
|
+
if (values.from) body.from = values.from;
|
|
170
|
+
if (values.subject) body.subject = values.subject;
|
|
171
|
+
if (values["reply-to"]) body.replyTo = values["reply-to"];
|
|
172
|
+
if (values.category) body.category = values.category;
|
|
173
|
+
if (values["skip-preference-check"]) body.skipPreferenceCheck = true;
|
|
174
|
+
if (values["idempotency-key"]) {
|
|
175
|
+
body.idempotencyKey = values["idempotency-key"];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
let res: SendResponse;
|
|
179
|
+
try {
|
|
180
|
+
res = await ctx.out.step(`Sending ${template}`, () =>
|
|
181
|
+
ctx.dataHttp.post<SendResponse>("/v1/emails", body),
|
|
182
|
+
);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
if (isHttpError(error)) {
|
|
185
|
+
ctx.out.fail(error.message);
|
|
186
|
+
}
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (ctx.json) {
|
|
191
|
+
ctx.out.json(res);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
ctx.out.intro(`${badge} send`);
|
|
196
|
+
ctx.out.kv(
|
|
197
|
+
{
|
|
198
|
+
emailSendId: res.emailSendId,
|
|
199
|
+
template,
|
|
200
|
+
recipient: to ?? userId ?? "",
|
|
201
|
+
status: statusColor(res.status),
|
|
202
|
+
reason: res.reason ?? "",
|
|
203
|
+
},
|
|
204
|
+
"Email send",
|
|
205
|
+
);
|
|
206
|
+
ctx.out.outro(`${template} → ${statusColor(res.status)}.`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function run(ctx: CommandContext): Promise<void> {
|
|
210
|
+
const sub = ctx.argv[0];
|
|
211
|
+
|
|
212
|
+
switch (sub) {
|
|
213
|
+
case "send":
|
|
214
|
+
// Strip the leading "send" token; the rest is send's own args.
|
|
215
|
+
return runSend(ctx, ctx.argv.slice(1));
|
|
216
|
+
case undefined:
|
|
217
|
+
ctx.out.fail(
|
|
218
|
+
"emails requires a subcommand: send (see hogsend emails --help)",
|
|
219
|
+
);
|
|
220
|
+
break;
|
|
221
|
+
default:
|
|
222
|
+
ctx.out.fail(`unknown emails subcommand "${sub}" — expected send`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export const emailsCommand: Command = {
|
|
227
|
+
name: "emails",
|
|
228
|
+
summary: "Send a transactional email through the data plane",
|
|
229
|
+
usage,
|
|
230
|
+
run,
|
|
231
|
+
};
|