shipmail-cli 0.6.6 → 0.6.7

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 (3) hide show
  1. package/README.md +32 -1
  2. package/dist/cli.js +282 -12
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -149,6 +149,20 @@ shipmail audiences welcome add-step aud_123 --subject "Welcome" --markdown @welc
149
149
  shipmail audiences welcome add-step aud_123 --subject "Our best posts" --markdown @day-3.md --wait-minutes 4320
150
150
  shipmail audiences welcome enable aud_123
151
151
 
152
+ # Store a template, publish it, then send it by ID
153
+ shipmail templates create \
154
+ --name "Welcome" \
155
+ --subject "Welcome, {{name}}" \
156
+ --html @welcome.html \
157
+ --fallback plan=Free \
158
+ --publish
159
+ shipmail templates render tpl_123 --var name=Ada
160
+ shipmail messages send --from hello@example.com --to ada@example.com --template tpl_123 --var name=Ada
161
+ shipmail templates update tpl_123 --subject "Welcome aboard, {{name}}"
162
+ shipmail templates publish tpl_123
163
+ shipmail templates versions tpl_123
164
+ shipmail templates restore tpl_123 1
165
+
152
166
  # Manage webhooks
153
167
  shipmail webhooks create \
154
168
  --url https://example.com/shipmail/webhook \
@@ -170,6 +184,11 @@ Shipmail converts headings, paragraphs, lists, quotes, code, images, links, divi
170
184
  to blocks. A callout starts with a line `:::callout` (optionally `info`, `warning` or `success`,
171
185
  then a title) and ends with a line `:::`.
172
186
 
187
+ With `messages send --template`, the template supplies the subject and body, so `--subject`,
188
+ `--html` and `--text` are refused. `--var key=value` sets a variable (repeat it; values are text),
189
+ and `--template-version` pins a version instead of the published one. A send missing a required
190
+ variable fails with HTTP 422 and prints each detail with its code, such as `missing_variable`.
191
+
173
192
  `newsletter-assets upload` uses a five-minute direct storage PUT, then calls the single-use
174
193
  completion URL. Video uploads require a JPEG poster through `--thumbnail`. `newsletter-assets list`
175
194
  includes current newsletter media storage usage and a `pagination.next_cursor` value in the JSON
@@ -191,7 +210,9 @@ Use repeatable `--header "Name: value"` flags for request headers. Use `--output
191
210
  - `api <METHOD> <PATH>`
192
211
  - `domains list|search|create|get|verify|delete`
193
212
  - `mailboxes list|create|get|update|suspend|resume|forwarding|forwarding-add|forwarding-remove|app-passwords|folders|identities|stage-attachment|prepare-attachment-upload`
194
- - `members list|get`
213
+ - `members list|get|invite`
214
+ - `members invitations list|cancel`
215
+ - `members mailboxes grant|revoke`
195
216
  - `inbox list|message|security-review|threads|thread|thread-state|draft|send-draft|reply-message|reply-thread|update|move`
196
217
  - `reply-scans create|status|results`
197
218
  - `messages send|list|analytics|get|reply` (test keys also support `--sandbox-outcome`)
@@ -199,6 +220,7 @@ Use repeatable `--header "Name: value"` flags for request headers. Use `--output
199
220
  - `mailboxes sandbox-inbound`
200
221
  - `webhooks list|create|update|test|deliveries|replay-delivery`
201
222
  - `suppressions add|list|remove`
223
+ - `templates list|create|get|update|delete|publish|render|versions|version|restore` (`messages send --template` sends one)
202
224
  - `audiences list|create|get|update|delete`
203
225
  - `audiences feed get|update|rotate|revoke`
204
226
  - `audiences welcome get|enable|disable|add-step|update-step|remove-step`
@@ -212,6 +234,15 @@ Member output uses the roles `owner`, `admin`, and `member`. Its `mailbox_access
212
234
  `selected`; `mailbox_ids` is empty for `all`, while selected IDs may be limited to mailboxes the
213
235
  API key can reach.
214
236
 
237
+ `members invite`, `members invitations cancel` and `members mailboxes grant|revoke` need the
238
+ `members:write` permission, which no preset includes. The invitee receives an email and creates
239
+ their own account; the CLI never receives their credentials:
240
+
241
+ ```bash
242
+ shipmail members invite sam@example.com --role member --mailbox mbx_support --newsletter-editor
243
+ shipmail members mailboxes grant mem_sam mbx_sales
244
+ ```
245
+
215
246
  Configure a publisher-owned Atom self URL and the number of recent newsletters:
216
247
 
217
248
  ```bash
package/dist/cli.js CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  ReconciliationRequiredError,
13
13
  ShipmailClient,
14
14
  ShipmailError,
15
+ ValidationError,
15
16
  WEBHOOK_EVENT_TYPES
16
17
  } from "shipmail";
17
18
 
@@ -651,7 +652,7 @@ async function login(options) {
651
652
  }
652
653
 
653
654
  // src/version.ts
654
- var VERSION = "0.6.6";
655
+ var VERSION = "0.6.7";
655
656
 
656
657
  // src/cli.ts
657
658
  var DEFAULT_BASE_URL = "https://shipmail.to/api/v1";
@@ -774,6 +775,20 @@ function getStringList(options, key) {
774
775
  if (isStringArray(value)) return value.flatMap(splitValues);
775
776
  return splitValues(value);
776
777
  }
778
+ function getKeyValues(options, key) {
779
+ const value = optionValue(options, key);
780
+ if (value === void 0) return void 0;
781
+ const entries = isStringArray(value) ? value : [value];
782
+ const result = {};
783
+ for (const entry of entries) {
784
+ const index = typeof entry === "string" ? entry.indexOf("=") : -1;
785
+ if (typeof entry !== "string" || index <= 0) {
786
+ throw new CliUsageError(`--${key} must use key=value format.`);
787
+ }
788
+ result[entry.slice(0, index).trim()] = entry.slice(index + 1);
789
+ }
790
+ return result;
791
+ }
777
792
  function validateBaseUrl(rawValue, allowInsecure) {
778
793
  let parsed;
779
794
  try {
@@ -1255,6 +1270,11 @@ Usage:
1255
1270
  shipmail mailboxes sandbox-inbound <id> --from sender@example.com --subject "Test" (--text "Hello" | --html "<p>Hello</p>") [--to support@example.com]
1256
1271
  shipmail members list [--limit 25] [--cursor cur]
1257
1272
  shipmail members get <id>
1273
+ shipmail members invitations list
1274
+ shipmail members invite <email> --role admin|member [--all-mailboxes] [--mailbox <mailbox-id> ...] [--newsletter-editor] [--idempotency-key key]
1275
+ shipmail members invitations cancel <invitation-id>
1276
+ shipmail members mailboxes grant <member-id> <mailbox-id>
1277
+ shipmail members mailboxes revoke <member-id> <mailbox-id>
1258
1278
  shipmail inbox list <mailbox-id> [--folder-role inbox] [--search text] [--from text] [--to text] [--subject text] [--has-attachment [true|false]] [--after ISO] [--before ISO] [--limit 25] [--cursor cur]
1259
1279
  shipmail inbox message <mailbox-id> <message-id>
1260
1280
  shipmail inbox security-review <mailbox-id> <message-id> --reason "Reviewed" --expected-content-sha256 HASH
@@ -1268,15 +1288,16 @@ Usage:
1268
1288
  shipmail inbox update-draft <mailbox-id> <thread-id> <draft-id> (--text "Hello" | --html "<p>Hello</p>") (the new body replaces the old one)
1269
1289
  shipmail inbox delete-draft <mailbox-id> <thread-id> <draft-id>
1270
1290
  shipmail inbox send-draft <mailbox-id> <thread-id> <draft-id> [--expected-content-sha256 hash]
1271
- shipmail inbox reply-message <mailbox-id> <message-id> --to user@example.com (--text "Hello" | --html "<p>Hello</p>") [--track-reply true|false]
1272
- shipmail inbox reply-thread <mailbox-id> <thread-id> --to user@example.com (--text "Hello" | --html "<p>Hello</p>") [--track-reply true|false]
1291
+ shipmail inbox reply-message <mailbox-id> <message-id> --to user@example.com [--cc ...] [--bcc ...] (--text "Hello" | --html "<p>Hello</p>") [--track-reply true|false]
1292
+ shipmail inbox reply-thread <mailbox-id> <thread-id> --to user@example.com [--cc ...] [--bcc ...] (--text "Hello" | --html "<p>Hello</p>") [--track-reply true|false]
1273
1293
  shipmail inbox update <mailbox-id> <message-id> [--read true] [--starred false]
1274
1294
  shipmail inbox move <mailbox-id> <message-id> (--target-role archive | --target-folder-id fld)
1275
1295
  shipmail messages send (--mailbox-id mbx | --from 'Acme Support <hello@example.com>') --to user@example.com --subject "Hi" --text "Hello" [--track-reply true|false] [--sandbox-outcome delivered|bounced|complained] [--client-reference crm-123] [--metadata JSON|@file] [--source-rfc-message-id '<crm-123@example.com>'] [--header 'X-CRM-ID: 123']
1296
+ shipmail messages send (--mailbox-id mbx | --from hello@example.com) --to user@example.com --template tpl_... [--template-version 2] [--var name=Ada ...]
1276
1297
  shipmail messages list (--mailbox-id mbx | --client-reference crm-123) [--limit 25] [--cursor cur]
1277
1298
  shipmail messages analytics [--updated-after ISO] [--updated-before ISO] [--limit 100] [--cursor cur]
1278
1299
  shipmail messages get <message-id>
1279
- shipmail messages reply <message-id> --to user@example.com (--text "Hello" | --html "<p>Hello</p>") [--track-reply true|false] [--client-reference crm-reply-123]
1300
+ shipmail messages reply <message-id> --to user@example.com [--cc ...] [--bcc ...] (--text "Hello" | --html "<p>Hello</p>") [--track-reply true|false] [--client-reference crm-reply-123]
1280
1301
  shipmail scheduled-messages list [--include-held] [--search text] [--limit 100]
1281
1302
  shipmail scheduled-messages get <message-id>
1282
1303
  shipmail scheduled-messages update <message-id> --to user@example.com --subject "Hi" --scheduled-at ISO (--text "Hello" | --html "<p>Hello</p>") [--staged-attachment-id sat_...]
@@ -1291,6 +1312,16 @@ Usage:
1291
1312
  shipmail suppressions add <email> [--scope all|marketing]
1292
1313
  shipmail suppressions list [--scope all|marketing] [--limit 25] [--cursor cur]
1293
1314
  shipmail suppressions remove <email>
1315
+ shipmail templates list [--limit 25] [--cursor cur]
1316
+ shipmail templates create --name "Welcome" --subject "Welcome, {{name}}" (--html "<p>Hi {{name}}</p>" | --text "Hi {{name}}") [--fallback name=there ...] [--publish]
1317
+ shipmail templates get <template-id>
1318
+ shipmail templates update <template-id> [--name "..."] [--subject "..."] [--html "..." | --clear-html] [--text "..." | --clear-text] [--fallback key=value ... | --clear-fallbacks]
1319
+ shipmail templates delete <template-id>
1320
+ shipmail templates publish <template-id>
1321
+ shipmail templates render <template-id> [--var name=Ada ...] [--template-version 2]
1322
+ shipmail templates versions <template-id> [--limit 25] [--cursor cur]
1323
+ shipmail templates version <template-id> <version>
1324
+ shipmail templates restore <template-id> <version>
1294
1325
  shipmail newsletters create \\
1295
1326
  --audience-id aud_123 \\
1296
1327
  --sender-identity-id nwsid_456 \\
@@ -1365,6 +1396,17 @@ One-off booking links:
1365
1396
  Revoking a link stops it at once and cannot be undone. Other flags: --description,
1366
1397
  --buffer-minutes, --minimum-notice-minutes, --conferencing-provider, --mailbox.
1367
1398
 
1399
+ Templates:
1400
+ {{key}} in the subject, html or text is a variable. --html and --text take the value or @file.
1401
+ --fallback key=value Default for a variable. Repeat it. A variable without one is required
1402
+ at send time. On update, the given fallbacks replace all of them.
1403
+ --publish On create, also publish the draft as version 1.
1404
+ update changes the draft; publish makes it the next version, which sends use. restore copies a
1405
+ version into the draft without publishing it. render previews the draft, or --template-version.
1406
+ messages send --template <id> sends the published version, or --template-version. --var
1407
+ key=value sets a variable (repeat it; values are text). --subject, --html and --text are refused
1408
+ with --template.
1409
+
1368
1410
  Conversation IDs:
1369
1411
  <thread-id> accepts a conversation ID (thd_...) or the mail-server thread ID. Store the
1370
1412
  conversation_id every response carries. thread_id is deprecated and unchanged.
@@ -1373,6 +1415,11 @@ Member access output:
1373
1415
  role is owner, admin, or member. mailbox_access.scope is all or selected. mailbox_ids is empty
1374
1416
  for all; selected IDs may be limited to mailboxes the API key can reach.
1375
1417
 
1418
+ Member invitations and mailbox sharing:
1419
+ Needs the members:write permission, which is never granted by default. The invitee receives an
1420
+ email and creates their own account; no credential is returned. Without --all-mailboxes the
1421
+ invitation shares only the --mailbox IDs. --newsletter-editor applies to the member role only.
1422
+
1376
1423
  Newsletter styling:
1377
1424
  --styling-mode <styled|plain> styled applies Shipmail's email theme. plain sends your HTML without injected styles, width, or centering, so the reader's email client styles it.
1378
1425
 
@@ -1580,24 +1627,17 @@ async function sendMessageParams(options) {
1580
1627
  const cc = messageRecipients(options, "cc");
1581
1628
  const bcc = messageRecipients(options, "bcc");
1582
1629
  const replyTo = getString(options, "reply-to");
1583
- const subject = getRequiredString(options, "subject");
1584
- const text = getString(options, "text");
1585
- const html = getString(options, "html");
1586
1630
  const headers = outboundMessageHeaders(options);
1587
1631
  const metadata = await messageMetadata(options);
1588
1632
  if (!mailboxId && !from) throw new CliUsageError("Provide --mailbox-id or --from.");
1589
1633
  if (to.length === 0) throw new CliUsageError("Provide at least one --to recipient.");
1590
- if (!text && !html) throw new CliUsageError("Provide --text or --html.");
1591
- return {
1634
+ const common = {
1592
1635
  ...mailboxId ? { mailbox_id: mailboxId } : {},
1593
1636
  ...from ? { from } : {},
1594
1637
  to,
1595
1638
  ...cc.length > 0 ? { cc } : {},
1596
1639
  ...bcc.length > 0 ? { bcc } : {},
1597
1640
  ...replyTo ? { reply_to: replyTo } : {},
1598
- subject,
1599
- ...html ? { html } : {},
1600
- ...text ? { text } : {},
1601
1641
  ...getString(options, "client-reference") ? { client_reference: getString(options, "client-reference") } : {},
1602
1642
  ...metadata ? { metadata } : {},
1603
1643
  ...getString(options, "source-rfc-message-id") ? { source_rfc_message_id: getString(options, "source-rfc-message-id") } : {},
@@ -1616,10 +1656,44 @@ async function sendMessageParams(options) {
1616
1656
  ])
1617
1657
  } : {}
1618
1658
  };
1659
+ const templateId = getString(options, "template");
1660
+ const templateVersion = getNumber(options, "template-version");
1661
+ const variables = getKeyValues(options, "var");
1662
+ if (templateId) {
1663
+ for (const key of ["subject", "html", "text"]) {
1664
+ if (optionValue(options, key) !== void 0) {
1665
+ throw new CliUsageError(
1666
+ `--${key} cannot be used with --template: the template supplies the subject and body.`
1667
+ );
1668
+ }
1669
+ }
1670
+ return {
1671
+ ...common,
1672
+ template: {
1673
+ id: templateId,
1674
+ ...templateVersion !== void 0 ? { version: templateVersion } : {},
1675
+ ...variables ? { variables } : {}
1676
+ }
1677
+ };
1678
+ }
1679
+ if (templateVersion !== void 0 || variables) {
1680
+ throw new CliUsageError("--template-version and --var need --template.");
1681
+ }
1682
+ const subject = getRequiredString(options, "subject");
1683
+ const text = getString(options, "text");
1684
+ const html = getString(options, "html");
1685
+ if (!text && !html) throw new CliUsageError("Provide --text or --html.");
1686
+ return {
1687
+ ...common,
1688
+ subject,
1689
+ ...html ? { html } : {},
1690
+ ...text ? { text } : {}
1691
+ };
1619
1692
  }
1620
1693
  async function replyToMessageParams(options) {
1621
1694
  const to = messageRecipients(options, "to");
1622
1695
  const cc = messageRecipients(options, "cc");
1696
+ const bcc = messageRecipients(options, "bcc");
1623
1697
  const text = getString(options, "text");
1624
1698
  const html = getString(options, "html");
1625
1699
  const headers = outboundMessageHeaders(options);
@@ -1629,6 +1703,7 @@ async function replyToMessageParams(options) {
1629
1703
  return {
1630
1704
  to,
1631
1705
  ...cc.length > 0 ? { cc } : {},
1706
+ ...bcc.length > 0 ? { bcc } : {},
1632
1707
  ...html ? { html } : {},
1633
1708
  ...text ? { text } : {},
1634
1709
  ...getString(options, "client-reference") ? { client_reference: getString(options, "client-reference") } : {},
@@ -2532,6 +2607,45 @@ async function runMembers(client, parsed, out) {
2532
2607
  );
2533
2608
  return;
2534
2609
  }
2610
+ if (subcommand === "invite") {
2611
+ const idempotencyKey = getString(parsed.options, "idempotency-key");
2612
+ writeJson(
2613
+ out,
2614
+ await client.members.invite(
2615
+ inviteMemberParams(parsed),
2616
+ idempotencyKey ? { idempotencyKey } : void 0
2617
+ )
2618
+ );
2619
+ return;
2620
+ }
2621
+ if (subcommand === "invitations") {
2622
+ const command = requirePositional(parsed.positional, 2, "invitations command");
2623
+ if (command === "list") {
2624
+ writeJson(out, await client.members.listInvitations());
2625
+ return;
2626
+ }
2627
+ if (command === "cancel") {
2628
+ const invitationId = requirePositional(parsed.positional, 3, "invitation id");
2629
+ await client.members.cancelInvitation(invitationId);
2630
+ writeJson(out, { ok: true, id: invitationId });
2631
+ return;
2632
+ }
2633
+ throw new CliUsageError("Unknown members invitations command.");
2634
+ }
2635
+ if (subcommand === "mailboxes") {
2636
+ const command = requirePositional(parsed.positional, 2, "mailboxes command");
2637
+ const memberId2 = requirePositional(parsed.positional, 3, "member id");
2638
+ const mailboxId = requirePositional(parsed.positional, 4, "mailbox id");
2639
+ if (command === "grant") {
2640
+ writeJson(out, await client.members.grantMailboxAccess(memberId2, mailboxId));
2641
+ return;
2642
+ }
2643
+ if (command === "revoke") {
2644
+ writeJson(out, await client.members.revokeMailboxAccess(memberId2, mailboxId));
2645
+ return;
2646
+ }
2647
+ throw new CliUsageError("Unknown members mailboxes command.");
2648
+ }
2535
2649
  const memberId = requirePositional(parsed.positional, 2, "member id");
2536
2650
  if (subcommand === "get") {
2537
2651
  writeJson(out, await client.members.get(memberId));
@@ -2539,6 +2653,29 @@ async function runMembers(client, parsed, out) {
2539
2653
  }
2540
2654
  throw new CliUsageError("Unknown members command.");
2541
2655
  }
2656
+ var INVITATION_ROLES = ["admin", "member"];
2657
+ function inviteMemberParams(parsed) {
2658
+ const email = requirePositional(parsed.positional, 2, "email");
2659
+ const rawRole = getRequiredString(parsed.options, "role");
2660
+ const role = INVITATION_ROLES.find((candidate) => candidate === rawRole);
2661
+ if (!role) throw new CliUsageError("--role must be admin or member.");
2662
+ const allMailboxes = getBoolean(parsed.options, "all-mailboxes") === true;
2663
+ const mailboxIds = getStringList(parsed.options, "mailbox");
2664
+ if (allMailboxes && mailboxIds.length > 0) {
2665
+ throw new CliUsageError("Use either --all-mailboxes or --mailbox, not both.");
2666
+ }
2667
+ const newsletterEditor = getBoolean(parsed.options, "newsletter-editor") === true;
2668
+ if (newsletterEditor && role !== "member") {
2669
+ throw new CliUsageError("--newsletter-editor applies to the member role only.");
2670
+ }
2671
+ return {
2672
+ email,
2673
+ role,
2674
+ ...allMailboxes ? { mailbox_access: "all" } : {},
2675
+ ...mailboxIds.length > 0 ? { mailbox_ids: mailboxIds } : {},
2676
+ ...newsletterEditor ? { newsletter_role: "editor" } : {}
2677
+ };
2678
+ }
2542
2679
  var SUPPRESSION_SCOPES = ["all", "marketing"];
2543
2680
  async function runSuppressions(client, parsed, out) {
2544
2681
  const subcommand = parsed.positional[1];
@@ -2572,6 +2709,131 @@ async function runSuppressions(client, parsed, out) {
2572
2709
  }
2573
2710
  throw new CliUsageError("Unknown suppressions command.");
2574
2711
  }
2712
+ async function templateCreateParams(options) {
2713
+ const html = await readTextInput(getString(options, "html"));
2714
+ const text = await readTextInput(getString(options, "text"));
2715
+ const fallbacks = getKeyValues(options, "fallback");
2716
+ const publish = getBoolean(options, "publish");
2717
+ if (!html && !text) throw new CliUsageError("Provide --html or --text.");
2718
+ return {
2719
+ name: getRequiredString(options, "name"),
2720
+ subject: getRequiredString(options, "subject"),
2721
+ ...html ? { html } : {},
2722
+ ...text ? { text } : {},
2723
+ ...fallbacks ? { fallbacks } : {},
2724
+ ...publish !== void 0 ? { publish } : {}
2725
+ };
2726
+ }
2727
+ async function templateBodyUpdate(options, key) {
2728
+ const value = await readTextInput(getString(options, key));
2729
+ const clear = getBoolean(options, `clear-${key}`) === true;
2730
+ if (value !== void 0 && clear) {
2731
+ throw new CliUsageError(`Use either --${key} or --clear-${key}, not both.`);
2732
+ }
2733
+ return clear ? null : value;
2734
+ }
2735
+ async function templateUpdateParams(options) {
2736
+ const name = getString(options, "name");
2737
+ const subject = getString(options, "subject");
2738
+ const html = await templateBodyUpdate(options, "html");
2739
+ const text = await templateBodyUpdate(options, "text");
2740
+ const fallbacks = getKeyValues(options, "fallback");
2741
+ const clearFallbacks = getBoolean(options, "clear-fallbacks") === true;
2742
+ if (fallbacks && clearFallbacks) {
2743
+ throw new CliUsageError("Use either --fallback or --clear-fallbacks, not both.");
2744
+ }
2745
+ const params = {
2746
+ ...name !== void 0 ? { name } : {},
2747
+ ...subject !== void 0 ? { subject } : {},
2748
+ ...html !== void 0 ? { html } : {},
2749
+ ...text !== void 0 ? { text } : {},
2750
+ ...fallbacks ? { fallbacks } : clearFallbacks ? { fallbacks: {} } : {}
2751
+ };
2752
+ if (name === void 0 && subject === void 0 && html === void 0 && text === void 0 && params.fallbacks === void 0) {
2753
+ throw new CliUsageError("Provide at least one field to change.");
2754
+ }
2755
+ return params;
2756
+ }
2757
+ function templateVersionPositional(positional) {
2758
+ const raw = requirePositional(positional, 3, "version");
2759
+ const version = Number(raw);
2760
+ if (!Number.isInteger(version) || version < 1) {
2761
+ throw new CliUsageError("Version must be a positive integer.");
2762
+ }
2763
+ return version;
2764
+ }
2765
+ async function runTemplates(client, parsed, out) {
2766
+ const subcommand = parsed.positional[1];
2767
+ if (subcommand === "list") {
2768
+ writeJson(
2769
+ out,
2770
+ await client.templates.list({
2771
+ cursor: getString(parsed.options, "cursor"),
2772
+ limit: getNumber(parsed.options, "limit")
2773
+ })
2774
+ );
2775
+ return;
2776
+ }
2777
+ if (subcommand === "create") {
2778
+ writeJson(out, await client.templates.create(await templateCreateParams(parsed.options)));
2779
+ return;
2780
+ }
2781
+ const id = requirePositional(parsed.positional, 2, "template id");
2782
+ if (subcommand === "get") {
2783
+ writeJson(out, await client.templates.get(id));
2784
+ return;
2785
+ }
2786
+ if (subcommand === "update") {
2787
+ writeJson(out, await client.templates.update(id, await templateUpdateParams(parsed.options)));
2788
+ return;
2789
+ }
2790
+ if (subcommand === "delete") {
2791
+ await client.templates.delete(id);
2792
+ writeJson(out, { ok: true, id });
2793
+ return;
2794
+ }
2795
+ if (subcommand === "publish") {
2796
+ writeJson(out, await client.templates.publish(id));
2797
+ return;
2798
+ }
2799
+ if (subcommand === "render") {
2800
+ const variables = getKeyValues(parsed.options, "var");
2801
+ const version = getNumber(parsed.options, "template-version");
2802
+ writeJson(
2803
+ out,
2804
+ await client.templates.render(id, {
2805
+ ...variables ? { variables } : {},
2806
+ ...version !== void 0 ? { version } : {}
2807
+ })
2808
+ );
2809
+ return;
2810
+ }
2811
+ if (subcommand === "versions") {
2812
+ writeJson(
2813
+ out,
2814
+ await client.templates.listVersions(id, {
2815
+ cursor: getString(parsed.options, "cursor"),
2816
+ limit: getNumber(parsed.options, "limit")
2817
+ })
2818
+ );
2819
+ return;
2820
+ }
2821
+ if (subcommand === "version") {
2822
+ writeJson(
2823
+ out,
2824
+ await client.templates.getVersion(id, templateVersionPositional(parsed.positional))
2825
+ );
2826
+ return;
2827
+ }
2828
+ if (subcommand === "restore") {
2829
+ writeJson(
2830
+ out,
2831
+ await client.templates.restoreVersion(id, templateVersionPositional(parsed.positional))
2832
+ );
2833
+ return;
2834
+ }
2835
+ throw new CliUsageError("Unknown templates command.");
2836
+ }
2575
2837
  var SUBSCRIBER_STATUS_VALUES = [
2576
2838
  "subscribed",
2577
2839
  "pending",
@@ -3520,6 +3782,13 @@ ${JSON.stringify(err.body, null, 2)}
3520
3782
  const messageId = shipmailError instanceof ReconciliationRequiredError && shipmailError.messageId ? ` message_id=${shipmailError.messageId}` : "";
3521
3783
  errOut.write(`Error: ${shipmailError.message}${status}${requestId}${messageId}
3522
3784
  `);
3785
+ if (shipmailError instanceof ValidationError) {
3786
+ for (const detail of shipmailError.details ?? []) {
3787
+ const code = detail.code ? ` (${detail.code})` : "";
3788
+ errOut.write(` ${detail.field}: ${detail.message}${code}
3789
+ `);
3790
+ }
3791
+ }
3523
3792
  if (shipmailError.actionUrl) {
3524
3793
  const reason = shipmailError.reason ? ` (${shipmailError.reason})` : "";
3525
3794
  errOut.write(`Ask the organization owner to open${reason}: ${shipmailError.actionUrl}
@@ -3603,6 +3872,7 @@ async function runCli(argv, options = {}) {
3603
3872
  else if (command === "reply-scans") await runReplyScans(client, parsed, out);
3604
3873
  else if (command === "webhooks") await runWebhooks(client, parsed, out);
3605
3874
  else if (command === "suppressions") await runSuppressions(client, parsed, out);
3875
+ else if (command === "templates") await runTemplates(client, parsed, out);
3606
3876
  else if (command === "newsletters") await runNewsletters(client, parsed, out);
3607
3877
  else if (command === "newsletter-assets")
3608
3878
  await runNewsletterAssets(client, parsed, out, options.fetch ?? globalThis.fetch);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shipmail-cli",
3
- "version": "0.6.6",
3
+ "version": "0.6.7",
4
4
  "description": "Official CLI for the Shipmail API",
5
5
  "type": "module",
6
6
  "bin": {
@@ -44,7 +44,7 @@
44
44
  "access": "public"
45
45
  },
46
46
  "dependencies": {
47
- "shipmail": "0.6.7",
47
+ "shipmail": "0.6.8",
48
48
  "zod": "^4.6.5"
49
49
  },
50
50
  "devDependencies": {