shipmail-cli 0.6.5 → 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 +38 -1
  2. package/dist/cli.js +285 -12
  3. package/package.json +6 -3
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
@@ -221,3 +252,9 @@ shipmail audiences feed update aud_123 \
221
252
  ```
222
253
 
223
254
  Run `shipmail --help` for flags and examples.
255
+
256
+ ## Links
257
+
258
+ - [CLI guide](https://shipmail.to/docs/cli)
259
+ - [Source](https://github.com/shipmail-to/shipmail-node/tree/main/packages/shipmail-cli)
260
+ - [Issues](https://github.com/shipmail-to/shipmail-node/issues)
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
 
@@ -351,6 +352,7 @@ var OAuthError = class extends Error {
351
352
  this.code = code;
352
353
  this.name = "OAuthError";
353
354
  }
355
+ code;
354
356
  };
355
357
  function trimSlash(value) {
356
358
  return value.replace(/\/+$/, "");
@@ -650,7 +652,7 @@ async function login(options) {
650
652
  }
651
653
 
652
654
  // src/version.ts
653
- var VERSION = "0.6.5";
655
+ var VERSION = "0.6.7";
654
656
 
655
657
  // src/cli.ts
656
658
  var DEFAULT_BASE_URL = "https://shipmail.to/api/v1";
@@ -686,6 +688,8 @@ var CliHttpError = class extends Error {
686
688
  this.body = body;
687
689
  this.name = "CliHttpError";
688
690
  }
691
+ status;
692
+ body;
689
693
  };
690
694
  function parseCliArgs(argv) {
691
695
  const positional = [];
@@ -771,6 +775,20 @@ function getStringList(options, key) {
771
775
  if (isStringArray(value)) return value.flatMap(splitValues);
772
776
  return splitValues(value);
773
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
+ }
774
792
  function validateBaseUrl(rawValue, allowInsecure) {
775
793
  let parsed;
776
794
  try {
@@ -1252,6 +1270,11 @@ Usage:
1252
1270
  shipmail mailboxes sandbox-inbound <id> --from sender@example.com --subject "Test" (--text "Hello" | --html "<p>Hello</p>") [--to support@example.com]
1253
1271
  shipmail members list [--limit 25] [--cursor cur]
1254
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>
1255
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]
1256
1279
  shipmail inbox message <mailbox-id> <message-id>
1257
1280
  shipmail inbox security-review <mailbox-id> <message-id> --reason "Reviewed" --expected-content-sha256 HASH
@@ -1265,15 +1288,16 @@ Usage:
1265
1288
  shipmail inbox update-draft <mailbox-id> <thread-id> <draft-id> (--text "Hello" | --html "<p>Hello</p>") (the new body replaces the old one)
1266
1289
  shipmail inbox delete-draft <mailbox-id> <thread-id> <draft-id>
1267
1290
  shipmail inbox send-draft <mailbox-id> <thread-id> <draft-id> [--expected-content-sha256 hash]
1268
- shipmail inbox reply-message <mailbox-id> <message-id> --to user@example.com (--text "Hello" | --html "<p>Hello</p>") [--track-reply true|false]
1269
- 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]
1270
1293
  shipmail inbox update <mailbox-id> <message-id> [--read true] [--starred false]
1271
1294
  shipmail inbox move <mailbox-id> <message-id> (--target-role archive | --target-folder-id fld)
1272
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 ...]
1273
1297
  shipmail messages list (--mailbox-id mbx | --client-reference crm-123) [--limit 25] [--cursor cur]
1274
1298
  shipmail messages analytics [--updated-after ISO] [--updated-before ISO] [--limit 100] [--cursor cur]
1275
1299
  shipmail messages get <message-id>
1276
- 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]
1277
1301
  shipmail scheduled-messages list [--include-held] [--search text] [--limit 100]
1278
1302
  shipmail scheduled-messages get <message-id>
1279
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_...]
@@ -1288,6 +1312,16 @@ Usage:
1288
1312
  shipmail suppressions add <email> [--scope all|marketing]
1289
1313
  shipmail suppressions list [--scope all|marketing] [--limit 25] [--cursor cur]
1290
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>
1291
1325
  shipmail newsletters create \\
1292
1326
  --audience-id aud_123 \\
1293
1327
  --sender-identity-id nwsid_456 \\
@@ -1362,6 +1396,17 @@ One-off booking links:
1362
1396
  Revoking a link stops it at once and cannot be undone. Other flags: --description,
1363
1397
  --buffer-minutes, --minimum-notice-minutes, --conferencing-provider, --mailbox.
1364
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
+
1365
1410
  Conversation IDs:
1366
1411
  <thread-id> accepts a conversation ID (thd_...) or the mail-server thread ID. Store the
1367
1412
  conversation_id every response carries. thread_id is deprecated and unchanged.
@@ -1370,6 +1415,11 @@ Member access output:
1370
1415
  role is owner, admin, or member. mailbox_access.scope is all or selected. mailbox_ids is empty
1371
1416
  for all; selected IDs may be limited to mailboxes the API key can reach.
1372
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
+
1373
1423
  Newsletter styling:
1374
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.
1375
1425
 
@@ -1577,24 +1627,17 @@ async function sendMessageParams(options) {
1577
1627
  const cc = messageRecipients(options, "cc");
1578
1628
  const bcc = messageRecipients(options, "bcc");
1579
1629
  const replyTo = getString(options, "reply-to");
1580
- const subject = getRequiredString(options, "subject");
1581
- const text = getString(options, "text");
1582
- const html = getString(options, "html");
1583
1630
  const headers = outboundMessageHeaders(options);
1584
1631
  const metadata = await messageMetadata(options);
1585
1632
  if (!mailboxId && !from) throw new CliUsageError("Provide --mailbox-id or --from.");
1586
1633
  if (to.length === 0) throw new CliUsageError("Provide at least one --to recipient.");
1587
- if (!text && !html) throw new CliUsageError("Provide --text or --html.");
1588
- return {
1634
+ const common = {
1589
1635
  ...mailboxId ? { mailbox_id: mailboxId } : {},
1590
1636
  ...from ? { from } : {},
1591
1637
  to,
1592
1638
  ...cc.length > 0 ? { cc } : {},
1593
1639
  ...bcc.length > 0 ? { bcc } : {},
1594
1640
  ...replyTo ? { reply_to: replyTo } : {},
1595
- subject,
1596
- ...html ? { html } : {},
1597
- ...text ? { text } : {},
1598
1641
  ...getString(options, "client-reference") ? { client_reference: getString(options, "client-reference") } : {},
1599
1642
  ...metadata ? { metadata } : {},
1600
1643
  ...getString(options, "source-rfc-message-id") ? { source_rfc_message_id: getString(options, "source-rfc-message-id") } : {},
@@ -1613,10 +1656,44 @@ async function sendMessageParams(options) {
1613
1656
  ])
1614
1657
  } : {}
1615
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
+ };
1616
1692
  }
1617
1693
  async function replyToMessageParams(options) {
1618
1694
  const to = messageRecipients(options, "to");
1619
1695
  const cc = messageRecipients(options, "cc");
1696
+ const bcc = messageRecipients(options, "bcc");
1620
1697
  const text = getString(options, "text");
1621
1698
  const html = getString(options, "html");
1622
1699
  const headers = outboundMessageHeaders(options);
@@ -1626,6 +1703,7 @@ async function replyToMessageParams(options) {
1626
1703
  return {
1627
1704
  to,
1628
1705
  ...cc.length > 0 ? { cc } : {},
1706
+ ...bcc.length > 0 ? { bcc } : {},
1629
1707
  ...html ? { html } : {},
1630
1708
  ...text ? { text } : {},
1631
1709
  ...getString(options, "client-reference") ? { client_reference: getString(options, "client-reference") } : {},
@@ -2529,6 +2607,45 @@ async function runMembers(client, parsed, out) {
2529
2607
  );
2530
2608
  return;
2531
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
+ }
2532
2649
  const memberId = requirePositional(parsed.positional, 2, "member id");
2533
2650
  if (subcommand === "get") {
2534
2651
  writeJson(out, await client.members.get(memberId));
@@ -2536,6 +2653,29 @@ async function runMembers(client, parsed, out) {
2536
2653
  }
2537
2654
  throw new CliUsageError("Unknown members command.");
2538
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
+ }
2539
2679
  var SUPPRESSION_SCOPES = ["all", "marketing"];
2540
2680
  async function runSuppressions(client, parsed, out) {
2541
2681
  const subcommand = parsed.positional[1];
@@ -2569,6 +2709,131 @@ async function runSuppressions(client, parsed, out) {
2569
2709
  }
2570
2710
  throw new CliUsageError("Unknown suppressions command.");
2571
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
+ }
2572
2837
  var SUBSCRIBER_STATUS_VALUES = [
2573
2838
  "subscribed",
2574
2839
  "pending",
@@ -3517,6 +3782,13 @@ ${JSON.stringify(err.body, null, 2)}
3517
3782
  const messageId = shipmailError instanceof ReconciliationRequiredError && shipmailError.messageId ? ` message_id=${shipmailError.messageId}` : "";
3518
3783
  errOut.write(`Error: ${shipmailError.message}${status}${requestId}${messageId}
3519
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
+ }
3520
3792
  if (shipmailError.actionUrl) {
3521
3793
  const reason = shipmailError.reason ? ` (${shipmailError.reason})` : "";
3522
3794
  errOut.write(`Ask the organization owner to open${reason}: ${shipmailError.actionUrl}
@@ -3600,6 +3872,7 @@ async function runCli(argv, options = {}) {
3600
3872
  else if (command === "reply-scans") await runReplyScans(client, parsed, out);
3601
3873
  else if (command === "webhooks") await runWebhooks(client, parsed, out);
3602
3874
  else if (command === "suppressions") await runSuppressions(client, parsed, out);
3875
+ else if (command === "templates") await runTemplates(client, parsed, out);
3603
3876
  else if (command === "newsletters") await runNewsletters(client, parsed, out);
3604
3877
  else if (command === "newsletter-assets")
3605
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.5",
3
+ "version": "0.6.7",
4
4
  "description": "Official CLI for the Shipmail API",
5
5
  "type": "module",
6
6
  "bin": {
@@ -31,9 +31,12 @@
31
31
  "homepage": "https://shipmail.to/docs/cli",
32
32
  "repository": {
33
33
  "type": "git",
34
- "url": "git+https://github.com/shipmail-to/Shipmail.git",
34
+ "url": "git+https://github.com/shipmail-to/shipmail-node.git",
35
35
  "directory": "packages/shipmail-cli"
36
36
  },
37
+ "bugs": {
38
+ "url": "https://github.com/shipmail-to/shipmail-node/issues"
39
+ },
37
40
  "engines": {
38
41
  "node": ">=18"
39
42
  },
@@ -41,7 +44,7 @@
41
44
  "access": "public"
42
45
  },
43
46
  "dependencies": {
44
- "shipmail": "0.6.6",
47
+ "shipmail": "0.6.8",
45
48
  "zod": "^4.6.5"
46
49
  },
47
50
  "devDependencies": {