shipmail-cli 0.1.3 → 0.1.5

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 +19 -0
  2. package/dist/cli.js +385 -5
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -40,6 +40,22 @@ shipmail messages send \
40
40
  shipmail inbox list mbx_123 --folder-role inbox --limit 25
41
41
  shipmail inbox thread mbx_123 thr_123
42
42
 
43
+ # Find newsletter sending domain IDs, then create a draft with blocks
44
+ shipmail newsletter-domains list
45
+ shipmail newsletters create \
46
+ --audience-id aud_123 \
47
+ --newsletter-domain-id nwsdom_456 \
48
+ --name "July changelog" \
49
+ --subject "What shipped in July" \
50
+ --blocks @blocks.json
51
+ shipmail newsletters update nws_123 --subject "Updated subject"
52
+ shipmail newsletters preview nws_123
53
+ shipmail newsletters test-send nws_123 --recipient-email owner@example.com
54
+ shipmail newsletters preflight nws_123
55
+ shipmail newsletters schedule nws_123 --scheduled-at 2026-08-01T09:00:00.000Z
56
+ shipmail newsletter-assets list --kind image
57
+ shipmail newsletter-assets upload ./hero.png --content-type image/png
58
+
43
59
  # Manage webhooks
44
60
  shipmail webhooks create \
45
61
  --url https://example.com/shipmail/webhook \
@@ -73,5 +89,8 @@ Use repeatable `--header "Name: value"` flags for request headers. Use `--output
73
89
  - `messages send|list|get`
74
90
  - `webhooks list|create|test|deliveries`
75
91
  - `suppressions list|remove`
92
+ - `newsletter-domains list`
93
+ - `newsletters create|update|preview|preflight|test-send|schedule|cancel|resume`
94
+ - `newsletter-assets list|upload`
76
95
 
77
96
  Run `shipmail --help` for flags and examples.
package/dist/cli.js CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  } from "shipmail";
13
13
 
14
14
  // src/version.ts
15
- var VERSION = "0.1.3";
15
+ var VERSION = "0.1.5";
16
16
 
17
17
  // src/cli.ts
18
18
  var DEFAULT_BASE_URL = "https://shipmail.to/api/v1";
@@ -149,9 +149,95 @@ async function readJsonInput(value) {
149
149
  try {
150
150
  return JSON.parse(raw);
151
151
  } catch {
152
- throw new CliUsageError("--data must be valid JSON or @path/to/file.json.");
152
+ throw new CliUsageError("Value must be valid JSON or @path/to/file.json.");
153
153
  }
154
154
  }
155
+ async function readTextInput(value) {
156
+ if (value === void 0) return void 0;
157
+ if (value.startsWith("@")) return readFile(value.slice(1), "utf8");
158
+ return value;
159
+ }
160
+ async function readNewsletterBlocks(value) {
161
+ const parsed = await readJsonInput(value);
162
+ if (!Array.isArray(parsed)) {
163
+ throw new CliUsageError("--blocks must be a JSON array of newsletter blocks.");
164
+ }
165
+ if (parsed.length === 0) {
166
+ throw new CliUsageError("--blocks must include at least one block.");
167
+ }
168
+ return parsed;
169
+ }
170
+ function parseArchiveVisibility(options) {
171
+ const value = getString(options, "archive-visibility");
172
+ if (value === void 0) return void 0;
173
+ if (value === "private" || value === "public") return value;
174
+ throw new CliUsageError("--archive-visibility must be private or public.");
175
+ }
176
+ async function newsletterCreateParams(options) {
177
+ const audience_id = getRequiredString(options, "audience-id");
178
+ const newsletter_domain_id = getRequiredString(options, "newsletter-domain-id");
179
+ const name = getRequiredString(options, "name");
180
+ const subject = getRequiredString(options, "subject");
181
+ const blocksInput = getString(options, "blocks");
182
+ const bodyHtmlInput = getString(options, "body-html");
183
+ const bodyTextInput = getString(options, "body-text");
184
+ const blocks = blocksInput ? await readNewsletterBlocks(blocksInput) : void 0;
185
+ const body_html = bodyHtmlInput ? await readTextInput(bodyHtmlInput) : void 0;
186
+ const body_text = bodyTextInput ? await readTextInput(bodyTextInput) : void 0;
187
+ if (!blocks?.length && !body_html?.trim() && !body_text?.trim()) {
188
+ throw new CliUsageError("Provide at least one of --blocks, --body-html, or --body-text.");
189
+ }
190
+ const send_window_hours = getNumber(options, "send-window-hours");
191
+ const archive_visibility = parseArchiveVisibility(options);
192
+ const preview_text = getString(options, "preview-text");
193
+ const from_name = getString(options, "from-name");
194
+ return {
195
+ audience_id,
196
+ newsletter_domain_id,
197
+ name,
198
+ subject,
199
+ ...preview_text !== void 0 ? { preview_text } : {},
200
+ ...from_name !== void 0 ? { from_name } : {},
201
+ ...blocks ? { blocks } : {},
202
+ ...body_html !== void 0 ? { body_html } : {},
203
+ ...body_text !== void 0 ? { body_text } : {},
204
+ ...send_window_hours !== void 0 ? { send_window_hours } : {},
205
+ ...archive_visibility !== void 0 ? { archive_visibility } : {}
206
+ };
207
+ }
208
+ async function newsletterUpdateParams(options) {
209
+ const audience_id = getString(options, "audience-id");
210
+ const newsletter_domain_id = getString(options, "newsletter-domain-id");
211
+ const name = getString(options, "name");
212
+ const subject = getString(options, "subject");
213
+ const preview_text = getString(options, "preview-text");
214
+ const from_name = getString(options, "from-name");
215
+ const send_window_hours = getNumber(options, "send-window-hours");
216
+ const archive_visibility = parseArchiveVisibility(options);
217
+ const blocksInput = getString(options, "blocks");
218
+ const bodyHtmlInput = getString(options, "body-html");
219
+ const bodyTextInput = getString(options, "body-text");
220
+ const blocks = blocksInput ? await readNewsletterBlocks(blocksInput) : void 0;
221
+ const body_html = bodyHtmlInput ? await readTextInput(bodyHtmlInput) : void 0;
222
+ const body_text = bodyTextInput ? await readTextInput(bodyTextInput) : void 0;
223
+ const params = {
224
+ ...audience_id ? { audience_id } : {},
225
+ ...newsletter_domain_id ? { newsletter_domain_id } : {},
226
+ ...name ? { name } : {},
227
+ ...subject ? { subject } : {},
228
+ ...preview_text !== void 0 ? { preview_text } : {},
229
+ ...from_name !== void 0 ? { from_name } : {},
230
+ ...blocks ? { blocks } : {},
231
+ ...body_html !== void 0 ? { body_html } : {},
232
+ ...body_text !== void 0 ? { body_text } : {},
233
+ ...send_window_hours !== void 0 ? { send_window_hours } : {},
234
+ ...archive_visibility !== void 0 ? { archive_visibility } : {}
235
+ };
236
+ if (Object.keys(params).length === 0) {
237
+ throw new CliUsageError("Provide at least one field to update.");
238
+ }
239
+ return params;
240
+ }
155
241
  function apiPath(rawPath) {
156
242
  if (!rawPath) throw new CliUsageError("Missing API path.");
157
243
  if (!rawPath.startsWith("/")) throw new CliUsageError("API path must start with '/'.");
@@ -242,6 +328,34 @@ Usage:
242
328
  shipmail webhooks create --url https://example.com/hook --events message.received,message.bounced
243
329
  shipmail suppressions list
244
330
  shipmail suppressions remove <email>
331
+ shipmail newsletters create \\
332
+ --audience-id aud_123 \\
333
+ --newsletter-domain-id nwsdom_456 \\
334
+ --name "July changelog" \\
335
+ --subject "What shipped in July" \\
336
+ --blocks @blocks.json
337
+ shipmail newsletters update nws_789 --subject "Updated subject" --blocks @blocks-v2.json
338
+ shipmail newsletters preview <id>
339
+ shipmail newsletters preflight <id>
340
+ shipmail newsletters test-send <id> --recipient-email owner@example.com
341
+ shipmail newsletters schedule <id> --scheduled-at 2026-08-01T09:00:00.000Z
342
+ shipmail newsletters cancel <id>
343
+ shipmail newsletters resume <id>
344
+ shipmail newsletter-assets list [--kind image|video] [--q text] [--limit 25] [--cursor cur]
345
+ shipmail newsletter-assets upload <file> [--filename name] [--content-type type]
346
+ shipmail newsletter-domains list [--limit 25] [--cursor cur]
347
+ shipmail audiences list [--limit 25] [--cursor cur]
348
+ shipmail audiences create <name> --consent-source "Signup form" [--description "..."]
349
+ shipmail audiences get <id>
350
+ shipmail audiences update <id> [--name "..."] [--description "..."]
351
+ shipmail audiences delete <id>
352
+ shipmail audiences subscribers add <audience-id> <email> [--name "Jane"] [--consent-source "..."]
353
+ shipmail audiences subscribers list <audience-id> [--status subscribed] [--email jane@x.com] [--limit 25]
354
+ shipmail audiences subscribers get <audience-id> <subscriber-id>
355
+ shipmail audiences subscribers by-email <audience-id> <email>
356
+ shipmail audiences subscribers update <audience-id> <subscriber-id> [--name "..."]
357
+ shipmail audiences subscribers unsubscribe <audience-id> <subscriber-id>
358
+ shipmail audiences subscribers remove <audience-id> <subscriber-id>
245
359
 
246
360
  Global options:
247
361
  --api-key <key> API key. Defaults to SHIPMAIL_API_KEY.
@@ -575,6 +689,267 @@ async function runSuppressions(client, parsed, out) {
575
689
  }
576
690
  throw new CliUsageError("Unknown suppressions command.");
577
691
  }
692
+ var SUBSCRIBER_STATUS_VALUES = [
693
+ "subscribed",
694
+ "unsubscribed",
695
+ "bounced",
696
+ "complained",
697
+ "suppressed",
698
+ "transactional_only"
699
+ ];
700
+ function subscriberListParams(options) {
701
+ const email = getString(options, "email");
702
+ const statusRaw = getString(options, "status");
703
+ const status = statusRaw ? SUBSCRIBER_STATUS_VALUES.find((s) => s === statusRaw) : void 0;
704
+ if (statusRaw && !status) {
705
+ throw new CliUsageError(`Invalid --status: ${statusRaw}.`);
706
+ }
707
+ return {
708
+ cursor: getString(options, "cursor"),
709
+ limit: getNumber(options, "limit"),
710
+ ...status ? { status } : {},
711
+ ...email ? { email } : {}
712
+ };
713
+ }
714
+ async function runSubscribers(client, parsed, out) {
715
+ const action = parsed.positional[2];
716
+ const audienceId = requirePositional(parsed.positional, 3, "audience id");
717
+ if (action === "list") {
718
+ writeJson(
719
+ out,
720
+ await client.audiences.subscribers.list(audienceId, subscriberListParams(parsed.options))
721
+ );
722
+ return;
723
+ }
724
+ if (action === "add") {
725
+ const name = getString(parsed.options, "name");
726
+ const consentSource = getString(parsed.options, "consent-source");
727
+ const consentIp = getString(parsed.options, "consent-ip");
728
+ writeJson(
729
+ out,
730
+ await client.audiences.subscribers.add(audienceId, {
731
+ email_address: requirePositional(parsed.positional, 4, "email"),
732
+ ...name ? { display_name: name } : {},
733
+ ...consentSource ? { consent_source: consentSource } : {},
734
+ ...consentIp ? { consent_ip: consentIp } : {}
735
+ })
736
+ );
737
+ return;
738
+ }
739
+ if (action === "get") {
740
+ writeJson(
741
+ out,
742
+ await client.audiences.subscribers.get(
743
+ audienceId,
744
+ requirePositional(parsed.positional, 4, "subscriber id")
745
+ )
746
+ );
747
+ return;
748
+ }
749
+ if (action === "by-email") {
750
+ writeJson(
751
+ out,
752
+ await client.audiences.subscribers.getByEmail(
753
+ audienceId,
754
+ requirePositional(parsed.positional, 4, "email")
755
+ )
756
+ );
757
+ return;
758
+ }
759
+ if (action === "update") {
760
+ const subscriberId = requirePositional(parsed.positional, 4, "subscriber id");
761
+ const name = getString(parsed.options, "name");
762
+ writeJson(
763
+ out,
764
+ await client.audiences.subscribers.update(audienceId, subscriberId, {
765
+ ...name ? { display_name: name } : {}
766
+ })
767
+ );
768
+ return;
769
+ }
770
+ if (action === "unsubscribe") {
771
+ writeJson(
772
+ out,
773
+ await client.audiences.subscribers.unsubscribe(
774
+ audienceId,
775
+ requirePositional(parsed.positional, 4, "subscriber id")
776
+ )
777
+ );
778
+ return;
779
+ }
780
+ if (action === "remove") {
781
+ const subscriberId = requirePositional(parsed.positional, 4, "subscriber id");
782
+ await client.audiences.subscribers.remove(audienceId, subscriberId);
783
+ writeJson(out, { ok: true, id: subscriberId });
784
+ return;
785
+ }
786
+ throw new CliUsageError("Unknown audiences subscribers command.");
787
+ }
788
+ async function runAudiences(client, parsed, out) {
789
+ const subcommand = parsed.positional[1];
790
+ if (subcommand === "subscribers") {
791
+ await runSubscribers(client, parsed, out);
792
+ return;
793
+ }
794
+ if (subcommand === "list") {
795
+ writeJson(
796
+ out,
797
+ await client.audiences.list({
798
+ cursor: getString(parsed.options, "cursor"),
799
+ limit: getNumber(parsed.options, "limit")
800
+ })
801
+ );
802
+ return;
803
+ }
804
+ if (subcommand === "create") {
805
+ const description = getString(parsed.options, "description");
806
+ writeJson(
807
+ out,
808
+ await client.audiences.create({
809
+ name: requirePositional(parsed.positional, 2, "audience name"),
810
+ consent_source: getRequiredString(parsed.options, "consent-source"),
811
+ ...description ? { description } : {}
812
+ })
813
+ );
814
+ return;
815
+ }
816
+ if (subcommand === "get") {
817
+ writeJson(
818
+ out,
819
+ await client.audiences.get(requirePositional(parsed.positional, 2, "audience id"))
820
+ );
821
+ return;
822
+ }
823
+ if (subcommand === "update") {
824
+ const id = requirePositional(parsed.positional, 2, "audience id");
825
+ const name = getString(parsed.options, "name");
826
+ const description = getString(parsed.options, "description");
827
+ writeJson(
828
+ out,
829
+ await client.audiences.update(id, {
830
+ ...name ? { name } : {},
831
+ ...description ? { description } : {}
832
+ })
833
+ );
834
+ return;
835
+ }
836
+ if (subcommand === "delete") {
837
+ const id = requirePositional(parsed.positional, 2, "audience id");
838
+ await client.audiences.delete(id);
839
+ writeJson(out, { ok: true, id });
840
+ return;
841
+ }
842
+ throw new CliUsageError("Unknown audiences command.");
843
+ }
844
+ async function runNewsletterDomains(client, parsed, out) {
845
+ const subcommand = parsed.positional[1];
846
+ if (subcommand === "list") {
847
+ writeJson(
848
+ out,
849
+ await client.newsletters.domains.list({
850
+ cursor: getString(parsed.options, "cursor"),
851
+ limit: getNumber(parsed.options, "limit")
852
+ })
853
+ );
854
+ return;
855
+ }
856
+ throw new CliUsageError("Unknown newsletter-domains command.");
857
+ }
858
+ async function runNewsletters(client, parsed, out) {
859
+ const subcommand = parsed.positional[1];
860
+ if (subcommand === "create") {
861
+ writeJson(out, await client.newsletters.create(await newsletterCreateParams(parsed.options)));
862
+ return;
863
+ }
864
+ if (subcommand === "update") {
865
+ const id = requirePositional(parsed.positional, 2, "newsletter id");
866
+ writeJson(
867
+ out,
868
+ await client.newsletters.update(id, await newsletterUpdateParams(parsed.options))
869
+ );
870
+ return;
871
+ }
872
+ if (subcommand === "preview") {
873
+ writeJson(
874
+ out,
875
+ await client.newsletters.preview(requirePositional(parsed.positional, 2, "newsletter id"))
876
+ );
877
+ return;
878
+ }
879
+ if (subcommand === "preflight") {
880
+ writeJson(
881
+ out,
882
+ await client.newsletters.preflight(requirePositional(parsed.positional, 2, "newsletter id"))
883
+ );
884
+ return;
885
+ }
886
+ if (subcommand === "test-send") {
887
+ writeJson(
888
+ out,
889
+ await client.newsletters.sendTest(requirePositional(parsed.positional, 2, "newsletter id"), {
890
+ recipient_email: getRequiredString(parsed.options, "recipient-email")
891
+ })
892
+ );
893
+ return;
894
+ }
895
+ if (subcommand === "schedule") {
896
+ writeJson(
897
+ out,
898
+ await client.newsletters.schedule(requirePositional(parsed.positional, 2, "newsletter id"), {
899
+ scheduled_at: getRequiredString(parsed.options, "scheduled-at")
900
+ })
901
+ );
902
+ return;
903
+ }
904
+ if (subcommand === "cancel") {
905
+ writeJson(
906
+ out,
907
+ await client.newsletters.cancel(requirePositional(parsed.positional, 2, "newsletter id"))
908
+ );
909
+ return;
910
+ }
911
+ if (subcommand === "resume") {
912
+ writeJson(
913
+ out,
914
+ await client.newsletters.resume(requirePositional(parsed.positional, 2, "newsletter id"))
915
+ );
916
+ return;
917
+ }
918
+ throw new CliUsageError("Unknown newsletters command.");
919
+ }
920
+ async function runNewsletterAssets(client, parsed, out) {
921
+ const subcommand = parsed.positional[1];
922
+ if (subcommand === "list") {
923
+ const kind = getString(parsed.options, "kind");
924
+ if (kind !== void 0 && kind !== "image" && kind !== "video") {
925
+ throw new CliUsageError("--kind must be image or video.");
926
+ }
927
+ writeJson(
928
+ out,
929
+ await client.newsletters.assets.list({
930
+ cursor: getString(parsed.options, "cursor"),
931
+ limit: getNumber(parsed.options, "limit"),
932
+ kind,
933
+ q: getString(parsed.options, "q")
934
+ })
935
+ );
936
+ return;
937
+ }
938
+ if (subcommand === "upload") {
939
+ const filePath = requirePositional(parsed.positional, 2, "file");
940
+ const data = await readFile(realpathSync(filePath));
941
+ writeJson(
942
+ out,
943
+ await client.newsletters.assets.upload({
944
+ filename: getString(parsed.options, "filename") ?? filePath.split(/[\\/]/).at(-1) ?? "newsletter-asset",
945
+ data,
946
+ content_type: getString(parsed.options, "content-type")
947
+ })
948
+ );
949
+ return;
950
+ }
951
+ throw new CliUsageError("Unknown newsletter-assets command.");
952
+ }
578
953
  function writeError(err, errOut) {
579
954
  if (err instanceof CliUsageError) {
580
955
  errOut.write(`Error: ${err.message}
@@ -589,9 +964,10 @@ ${JSON.stringify(err.body, null, 2)}
589
964
  return;
590
965
  }
591
966
  if (err instanceof ShipMailError) {
592
- const status = err.status !== void 0 ? ` (HTTP ${err.status})` : "";
593
- const requestId = err.requestId ? ` request_id=${err.requestId}` : "";
594
- errOut.write(`Error: ${err.message}${status}${requestId}
967
+ const shipmailError = err;
968
+ const status = shipmailError.status !== void 0 ? ` (HTTP ${shipmailError.status})` : "";
969
+ const requestId = shipmailError.requestId ? ` request_id=${shipmailError.requestId}` : "";
970
+ errOut.write(`Error: ${shipmailError.message}${status}${requestId}
595
971
  `);
596
972
  return;
597
973
  }
@@ -633,6 +1009,10 @@ async function runCli(argv, options = {}) {
633
1009
  else if (command === "messages") await runMessages(client, parsed, out);
634
1010
  else if (command === "webhooks") await runWebhooks(client, parsed, out);
635
1011
  else if (command === "suppressions") await runSuppressions(client, parsed, out);
1012
+ else if (command === "newsletters") await runNewsletters(client, parsed, out);
1013
+ else if (command === "newsletter-assets") await runNewsletterAssets(client, parsed, out);
1014
+ else if (command === "newsletter-domains") await runNewsletterDomains(client, parsed, out);
1015
+ else if (command === "audiences") await runAudiences(client, parsed, out);
636
1016
  else throw new CliUsageError(`Unknown command: ${command}.`);
637
1017
  return 0;
638
1018
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shipmail-cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Official CLI for the ShipMail API",
5
5
  "type": "module",
6
6
  "bin": {
@@ -41,7 +41,7 @@
41
41
  "access": "public"
42
42
  },
43
43
  "dependencies": {
44
- "shipmail": "0.1.21"
44
+ "shipmail": "0.1.26"
45
45
  },
46
46
  "devDependencies": {
47
47
  "tsup": "^8.4.0",