shipmail-cli 0.1.4 → 0.1.6

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 +22 -0
  2. package/dist/cli.js +244 -5
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -40,6 +40,23 @@ 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
+ shipmail newsletter-assets register-url https://cdn.shipmail.to/newsletter-images/org_123/hero.png
59
+
43
60
  # Manage webhooks
44
61
  shipmail webhooks create \
45
62
  --url https://example.com/shipmail/webhook \
@@ -47,6 +64,8 @@ shipmail webhooks create \
47
64
  shipmail webhooks deliveries whk_123 --status failed
48
65
  ```
49
66
 
67
+ `newsletter-assets list` includes current newsletter media storage usage in the JSON response.
68
+
50
69
  ## Generic API Command
51
70
 
52
71
  The `api` command reaches every Shipmail REST endpoint, including endpoints that do not have a shorthand command yet.
@@ -73,5 +92,8 @@ Use repeatable `--header "Name: value"` flags for request headers. Use `--output
73
92
  - `messages send|list|get`
74
93
  - `webhooks list|create|test|deliveries`
75
94
  - `suppressions list|remove`
95
+ - `newsletter-domains list`
96
+ - `newsletters create|update|preview|preflight|test-send|schedule|cancel|resume`
97
+ - `newsletter-assets list|upload|register-url`
76
98
 
77
99
  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.4";
15
+ var VERSION = "0.1.6";
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,23 @@ 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-assets register-url <url> [--filename name] [--thumbnail-url url]
347
+ shipmail newsletter-domains list [--limit 25] [--cursor cur]
245
348
  shipmail audiences list [--limit 25] [--cursor cur]
246
349
  shipmail audiences create <name> --consent-source "Signup form" [--description "..."]
247
350
  shipmail audiences get <id>
@@ -253,6 +356,7 @@ Usage:
253
356
  shipmail audiences subscribers by-email <audience-id> <email>
254
357
  shipmail audiences subscribers update <audience-id> <subscriber-id> [--name "..."]
255
358
  shipmail audiences subscribers unsubscribe <audience-id> <subscriber-id>
359
+ shipmail audiences subscribers resubscribe <audience-id> <subscriber-id> --consent-source "Signup form"
256
360
  shipmail audiences subscribers remove <audience-id> <subscriber-id>
257
361
 
258
362
  Global options:
@@ -675,6 +779,17 @@ async function runSubscribers(client, parsed, out) {
675
779
  );
676
780
  return;
677
781
  }
782
+ if (action === "resubscribe") {
783
+ writeJson(
784
+ out,
785
+ await client.audiences.subscribers.resubscribe(
786
+ audienceId,
787
+ requirePositional(parsed.positional, 4, "subscriber id"),
788
+ { consent_source: getRequiredString(parsed.options, "consent-source") }
789
+ )
790
+ );
791
+ return;
792
+ }
678
793
  if (action === "remove") {
679
794
  const subscriberId = requirePositional(parsed.positional, 4, "subscriber id");
680
795
  await client.audiences.subscribers.remove(audienceId, subscriberId);
@@ -739,6 +854,126 @@ async function runAudiences(client, parsed, out) {
739
854
  }
740
855
  throw new CliUsageError("Unknown audiences command.");
741
856
  }
857
+ async function runNewsletterDomains(client, parsed, out) {
858
+ const subcommand = parsed.positional[1];
859
+ if (subcommand === "list") {
860
+ writeJson(
861
+ out,
862
+ await client.newsletters.domains.list({
863
+ cursor: getString(parsed.options, "cursor"),
864
+ limit: getNumber(parsed.options, "limit")
865
+ })
866
+ );
867
+ return;
868
+ }
869
+ throw new CliUsageError("Unknown newsletter-domains command.");
870
+ }
871
+ async function runNewsletters(client, parsed, out) {
872
+ const subcommand = parsed.positional[1];
873
+ if (subcommand === "create") {
874
+ writeJson(out, await client.newsletters.create(await newsletterCreateParams(parsed.options)));
875
+ return;
876
+ }
877
+ if (subcommand === "update") {
878
+ const id = requirePositional(parsed.positional, 2, "newsletter id");
879
+ writeJson(
880
+ out,
881
+ await client.newsletters.update(id, await newsletterUpdateParams(parsed.options))
882
+ );
883
+ return;
884
+ }
885
+ if (subcommand === "preview") {
886
+ writeJson(
887
+ out,
888
+ await client.newsletters.preview(requirePositional(parsed.positional, 2, "newsletter id"))
889
+ );
890
+ return;
891
+ }
892
+ if (subcommand === "preflight") {
893
+ writeJson(
894
+ out,
895
+ await client.newsletters.preflight(requirePositional(parsed.positional, 2, "newsletter id"))
896
+ );
897
+ return;
898
+ }
899
+ if (subcommand === "test-send") {
900
+ writeJson(
901
+ out,
902
+ await client.newsletters.sendTest(requirePositional(parsed.positional, 2, "newsletter id"), {
903
+ recipient_email: getRequiredString(parsed.options, "recipient-email")
904
+ })
905
+ );
906
+ return;
907
+ }
908
+ if (subcommand === "schedule") {
909
+ writeJson(
910
+ out,
911
+ await client.newsletters.schedule(requirePositional(parsed.positional, 2, "newsletter id"), {
912
+ scheduled_at: getRequiredString(parsed.options, "scheduled-at")
913
+ })
914
+ );
915
+ return;
916
+ }
917
+ if (subcommand === "cancel") {
918
+ writeJson(
919
+ out,
920
+ await client.newsletters.cancel(requirePositional(parsed.positional, 2, "newsletter id"))
921
+ );
922
+ return;
923
+ }
924
+ if (subcommand === "resume") {
925
+ writeJson(
926
+ out,
927
+ await client.newsletters.resume(requirePositional(parsed.positional, 2, "newsletter id"))
928
+ );
929
+ return;
930
+ }
931
+ throw new CliUsageError("Unknown newsletters command.");
932
+ }
933
+ async function runNewsletterAssets(client, parsed, out) {
934
+ const subcommand = parsed.positional[1];
935
+ if (subcommand === "list") {
936
+ const kind = getString(parsed.options, "kind");
937
+ if (kind !== void 0 && kind !== "image" && kind !== "video") {
938
+ throw new CliUsageError("--kind must be image or video.");
939
+ }
940
+ writeJson(
941
+ out,
942
+ await client.newsletters.assets.list({
943
+ cursor: getString(parsed.options, "cursor"),
944
+ limit: getNumber(parsed.options, "limit"),
945
+ kind,
946
+ q: getString(parsed.options, "q")
947
+ })
948
+ );
949
+ return;
950
+ }
951
+ if (subcommand === "upload") {
952
+ const filePath = requirePositional(parsed.positional, 2, "file");
953
+ const data = await readFile(realpathSync(filePath));
954
+ writeJson(
955
+ out,
956
+ await client.newsletters.assets.upload({
957
+ filename: getString(parsed.options, "filename") ?? filePath.split(/[\\/]/).at(-1) ?? "newsletter-asset",
958
+ data,
959
+ content_type: getString(parsed.options, "content-type")
960
+ })
961
+ );
962
+ return;
963
+ }
964
+ if (subcommand === "register-url") {
965
+ writeJson(
966
+ out,
967
+ await client.newsletters.assets.registerFromUrl({
968
+ url: requirePositional(parsed.positional, 2, "url"),
969
+ filename: getString(parsed.options, "filename"),
970
+ thumbnail_url: getString(parsed.options, "thumbnail-url")
971
+ })
972
+ );
973
+ return;
974
+ }
975
+ throw new CliUsageError("Unknown newsletter-assets command.");
976
+ }
742
977
  function writeError(err, errOut) {
743
978
  if (err instanceof CliUsageError) {
744
979
  errOut.write(`Error: ${err.message}
@@ -753,9 +988,10 @@ ${JSON.stringify(err.body, null, 2)}
753
988
  return;
754
989
  }
755
990
  if (err instanceof ShipMailError) {
756
- const status = err.status !== void 0 ? ` (HTTP ${err.status})` : "";
757
- const requestId = err.requestId ? ` request_id=${err.requestId}` : "";
758
- errOut.write(`Error: ${err.message}${status}${requestId}
991
+ const shipmailError = err;
992
+ const status = shipmailError.status !== void 0 ? ` (HTTP ${shipmailError.status})` : "";
993
+ const requestId = shipmailError.requestId ? ` request_id=${shipmailError.requestId}` : "";
994
+ errOut.write(`Error: ${shipmailError.message}${status}${requestId}
759
995
  `);
760
996
  return;
761
997
  }
@@ -797,6 +1033,9 @@ async function runCli(argv, options = {}) {
797
1033
  else if (command === "messages") await runMessages(client, parsed, out);
798
1034
  else if (command === "webhooks") await runWebhooks(client, parsed, out);
799
1035
  else if (command === "suppressions") await runSuppressions(client, parsed, out);
1036
+ else if (command === "newsletters") await runNewsletters(client, parsed, out);
1037
+ else if (command === "newsletter-assets") await runNewsletterAssets(client, parsed, out);
1038
+ else if (command === "newsletter-domains") await runNewsletterDomains(client, parsed, out);
800
1039
  else if (command === "audiences") await runAudiences(client, parsed, out);
801
1040
  else throw new CliUsageError(`Unknown command: ${command}.`);
802
1041
  return 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shipmail-cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
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.24"
44
+ "shipmail": "0.1.29"
45
45
  },
46
46
  "devDependencies": {
47
47
  "tsup": "^8.4.0",