shipmail-cli 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 ShipMail
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,77 @@
1
+ # ShipMail CLI
2
+
3
+ Official command line interface for the ShipMail API.
4
+
5
+ ```bash
6
+ npx -y shipmail-cli --help
7
+ ```
8
+
9
+ ## Authentication
10
+
11
+ Most commands require an API key. Create one in the ShipMail dashboard and export it:
12
+
13
+ ```bash
14
+ export SHIPMAIL_API_KEY="sm_live_..."
15
+ ```
16
+
17
+ You can also pass `--api-key` to any command. Use `SHIPMAIL_BASE_URL` or `--base-url` when targeting a non-production API.
18
+
19
+ ## Common Commands
20
+
21
+ ```bash
22
+ # Check public API health
23
+ shipmail status
24
+
25
+ # Create a domain and verify DNS
26
+ shipmail domains create example.com
27
+ shipmail domains verify dom_123
28
+
29
+ # Create a mailbox
30
+ shipmail mailboxes create --domain-id dom_123 --address hello --display-name "Hello"
31
+
32
+ # Send email
33
+ shipmail messages send \
34
+ --mailbox-id mbx_123 \
35
+ --to user@example.com \
36
+ --subject "Hello" \
37
+ --text "It works."
38
+
39
+ # Read inbound mail
40
+ shipmail inbox list mbx_123 --folder-role inbox --limit 25
41
+ shipmail inbox thread mbx_123 thr_123
42
+
43
+ # Manage webhooks
44
+ shipmail webhooks create \
45
+ --url https://example.com/shipmail/webhook \
46
+ --events message.received,message.bounced
47
+ shipmail webhooks deliveries whk_123 --status failed
48
+ ```
49
+
50
+ ## Generic API Command
51
+
52
+ The `api` command reaches every ShipMail REST endpoint, including endpoints that do not have a shorthand command yet.
53
+
54
+ ```bash
55
+ shipmail api GET /domains
56
+
57
+ shipmail api PATCH /mailboxes/mbx_123/spam-filter \
58
+ --data '{"threshold":6}'
59
+
60
+ shipmail api PUT /mailboxes/mbx_123/rules \
61
+ --data @rules.json
62
+ ```
63
+
64
+ Use repeatable `--header "Name: value"` flags for request headers. Use `--output path` to write non-JSON responses, such as attachment downloads.
65
+
66
+ ## Command Surface
67
+
68
+ - `status`
69
+ - `api <METHOD> <PATH>`
70
+ - `domains list|search|create|get|verify|delete`
71
+ - `mailboxes list|create|get|folders|identities|rules`
72
+ - `inbox list|thread|update|move`
73
+ - `messages send|list|get`
74
+ - `webhooks list|create|test|deliveries`
75
+ - `suppressions list|remove`
76
+
77
+ Run `shipmail --help` for flags and examples.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ type Output = {
3
+ write(chunk: string): void;
4
+ };
5
+ type CliEnv = Record<string, string | undefined>;
6
+ type RunCliOptions = {
7
+ readonly env?: CliEnv | undefined;
8
+ readonly stdout?: Output | undefined;
9
+ readonly stderr?: Output | undefined;
10
+ readonly fetch?: typeof fetch | undefined;
11
+ };
12
+ declare function runCli(argv: readonly string[], options?: RunCliOptions): Promise<number>;
13
+
14
+ export { runCli };
package/dist/cli.js ADDED
@@ -0,0 +1,651 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { readFile, writeFile } from "fs/promises";
5
+ import { env as processEnv, exit, stderr, stdout } from "process";
6
+ import { pathToFileURL } from "url";
7
+ import {
8
+ ShipMailClient,
9
+ ShipMailError,
10
+ WEBHOOK_EVENT_TYPES
11
+ } from "shipmail";
12
+
13
+ // src/version.ts
14
+ var VERSION = "0.1.0";
15
+
16
+ // src/cli.ts
17
+ var DEFAULT_BASE_URL = "https://shipmail.to/api/v1";
18
+ var WEBHOOK_EVENT_TYPE_SET = new Set(WEBHOOK_EVENT_TYPES);
19
+ var CliUsageError = class extends Error {
20
+ constructor(message) {
21
+ super(message);
22
+ this.name = "CliUsageError";
23
+ }
24
+ };
25
+ var CliHttpError = class extends Error {
26
+ constructor(status, body) {
27
+ super(`Request failed with status ${status}`);
28
+ this.status = status;
29
+ this.body = body;
30
+ this.name = "CliHttpError";
31
+ }
32
+ };
33
+ function parseCliArgs(argv) {
34
+ const positional = [];
35
+ const options = {};
36
+ for (let i = 0; i < argv.length; i += 1) {
37
+ const token = argv[i];
38
+ if (token === "--") {
39
+ positional.push(...argv.slice(i + 1));
40
+ break;
41
+ }
42
+ if (!token.startsWith("--") || token === "--") {
43
+ positional.push(token);
44
+ continue;
45
+ }
46
+ const raw = token.slice(2);
47
+ const inlineIndex = raw.indexOf("=");
48
+ const key = inlineIndex >= 0 ? raw.slice(0, inlineIndex) : raw;
49
+ const inlineValue = inlineIndex >= 0 ? raw.slice(inlineIndex + 1) : void 0;
50
+ const next = argv[i + 1];
51
+ let value;
52
+ if (inlineValue !== void 0) {
53
+ value = inlineValue;
54
+ } else if (next !== void 0 && !next.startsWith("--")) {
55
+ value = next;
56
+ i += 1;
57
+ } else {
58
+ value = true;
59
+ }
60
+ const existing = options[key];
61
+ if (existing === void 0) {
62
+ options[key] = value;
63
+ } else if (Array.isArray(existing)) {
64
+ options[key] = [...existing, String(value)];
65
+ } else {
66
+ options[key] = [String(existing), String(value)];
67
+ }
68
+ }
69
+ return { positional, options };
70
+ }
71
+ function optionValue(options, key) {
72
+ return options[key];
73
+ }
74
+ function isStringArray(value) {
75
+ return Array.isArray(value);
76
+ }
77
+ function getString(options, key) {
78
+ const value = optionValue(options, key);
79
+ if (value === void 0) return void 0;
80
+ if (isStringArray(value)) return value.at(-1);
81
+ if (typeof value === "boolean") return value ? "true" : "false";
82
+ return value;
83
+ }
84
+ function getRequiredString(options, key) {
85
+ const value = getString(options, key);
86
+ if (!value) throw new CliUsageError(`Missing --${key}.`);
87
+ return value;
88
+ }
89
+ function getNumber(options, key) {
90
+ const value = getString(options, key);
91
+ if (value === void 0) return void 0;
92
+ const parsed = Number(value);
93
+ if (!Number.isInteger(parsed)) throw new CliUsageError(`--${key} must be an integer.`);
94
+ return parsed;
95
+ }
96
+ function getBoolean(options, key) {
97
+ const value = optionValue(options, key);
98
+ if (value === void 0) return void 0;
99
+ const raw = isStringArray(value) ? value.at(-1) : value;
100
+ if (raw === true) return true;
101
+ if (raw === false) return false;
102
+ if (raw === "true" || raw === "1" || raw === "yes") return true;
103
+ if (raw === "false" || raw === "0" || raw === "no") return false;
104
+ throw new CliUsageError(`--${key} must be true or false.`);
105
+ }
106
+ function splitValues(value) {
107
+ if (typeof value === "boolean") return [];
108
+ return value.split(",").map((item) => item.trim()).filter(Boolean);
109
+ }
110
+ function getStringList(options, key) {
111
+ const value = optionValue(options, key);
112
+ if (value === void 0) return [];
113
+ if (isStringArray(value)) return value.flatMap(splitValues);
114
+ return splitValues(value);
115
+ }
116
+ function normalizeBaseUrl(value) {
117
+ return (value ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
118
+ }
119
+ function apiKey(parsed, env) {
120
+ return getString(parsed.options, "api-key") ?? env["SHIPMAIL_API_KEY"];
121
+ }
122
+ function baseUrl(parsed, env) {
123
+ return normalizeBaseUrl(getString(parsed.options, "base-url") ?? env["SHIPMAIL_BASE_URL"]);
124
+ }
125
+ function requireApiKey(parsed, env) {
126
+ const key = apiKey(parsed, env);
127
+ if (!key) {
128
+ throw new CliUsageError("Missing API key. Set SHIPMAIL_API_KEY or pass --api-key sm_live_...");
129
+ }
130
+ return key;
131
+ }
132
+ function createClient(parsed, options) {
133
+ const env = options.env ?? processEnv;
134
+ const config = {
135
+ apiKey: requireApiKey(parsed, env),
136
+ baseUrl: baseUrl(parsed, env),
137
+ ...options.fetch ? { fetch: options.fetch } : {}
138
+ };
139
+ return new ShipMailClient(config);
140
+ }
141
+ function writeJson(out, value) {
142
+ out.write(`${JSON.stringify(value, null, 2)}
143
+ `);
144
+ }
145
+ async function readJsonInput(value) {
146
+ if (value === void 0) return void 0;
147
+ const raw = value.startsWith("@") ? await readFile(value.slice(1), "utf8") : value;
148
+ try {
149
+ return JSON.parse(raw);
150
+ } catch {
151
+ throw new CliUsageError("--data must be valid JSON or @path/to/file.json.");
152
+ }
153
+ }
154
+ function apiPath(rawPath) {
155
+ if (!rawPath) throw new CliUsageError("Missing API path.");
156
+ if (!rawPath.startsWith("/")) throw new CliUsageError("API path must start with '/'.");
157
+ return rawPath;
158
+ }
159
+ function parseHeaders(values) {
160
+ const headers = {};
161
+ for (const value of values) {
162
+ const index = value.indexOf(":");
163
+ if (index <= 0) throw new CliUsageError("--header must use 'Name: value' format.");
164
+ headers[value.slice(0, index).trim()] = value.slice(index + 1).trim();
165
+ }
166
+ return headers;
167
+ }
168
+ async function rawApiRequest(parsed, runOptions, method, path, body, authRequired) {
169
+ const env = runOptions.env ?? processEnv;
170
+ const headers = {
171
+ Accept: "application/json",
172
+ ...parseHeaders(getStringList(parsed.options, "header"))
173
+ };
174
+ if (authRequired) {
175
+ headers["Authorization"] = `Bearer ${requireApiKey(parsed, env)}`;
176
+ } else {
177
+ const key = apiKey(parsed, env);
178
+ if (key) headers["Authorization"] = `Bearer ${key}`;
179
+ }
180
+ let requestBody;
181
+ if (body !== void 0) {
182
+ headers["Content-Type"] = "application/json";
183
+ requestBody = JSON.stringify(body);
184
+ }
185
+ const response = await (runOptions.fetch ?? fetch)(`${baseUrl(parsed, env)}${path}`, {
186
+ method,
187
+ headers,
188
+ body: requestBody
189
+ });
190
+ if (!response.ok) {
191
+ const contentType = response.headers.get("content-type") ?? "";
192
+ const errorBody = contentType.includes("application/json") ? await response.json().catch(() => null) : await response.text().catch(() => "");
193
+ throw new CliHttpError(response.status, errorBody);
194
+ }
195
+ return response;
196
+ }
197
+ async function writeRawResponse(parsed, io, response) {
198
+ const outputPath = getString(parsed.options, "output");
199
+ if (outputPath) {
200
+ await writeFile(outputPath, Buffer.from(await response.arrayBuffer()));
201
+ io.stdout.write(`${outputPath}
202
+ `);
203
+ return;
204
+ }
205
+ if (response.status === 204) {
206
+ writeJson(io.stdout, { ok: true, status: response.status });
207
+ return;
208
+ }
209
+ const contentType = response.headers.get("content-type") ?? "";
210
+ if (contentType.includes("application/json")) {
211
+ writeJson(io.stdout, await response.json());
212
+ return;
213
+ }
214
+ io.stdout.write(`${await response.text()}
215
+ `);
216
+ }
217
+ function usage() {
218
+ return `shipmail-cli v${VERSION}
219
+
220
+ Usage:
221
+ shipmail status
222
+ shipmail api <METHOD> <PATH> [--data JSON|@file] [--output file]
223
+ shipmail domains list [--limit 25] [--cursor cur]
224
+ shipmail domains search <keyword>
225
+ shipmail domains create <name>
226
+ shipmail domains get <id>
227
+ shipmail domains verify <id>
228
+ shipmail domains delete <id>
229
+ shipmail mailboxes list [--domain-id dom] [--limit 25] [--cursor cur]
230
+ shipmail mailboxes create --domain-id dom --address hello [--display-name "Hello"]
231
+ shipmail mailboxes get <id>
232
+ shipmail mailboxes folders <id>
233
+ shipmail mailboxes identities <id>
234
+ shipmail mailboxes rules <id>
235
+ shipmail inbox list <mailbox-id> [--folder-role inbox] [--limit 25]
236
+ shipmail inbox thread <mailbox-id> <thread-id>
237
+ shipmail inbox update <mailbox-id> <message-id> [--read true] [--starred false]
238
+ shipmail inbox move <mailbox-id> <message-id> (--target-role archive | --target-folder-id fld)
239
+ shipmail messages send --mailbox-id mbx --to user@example.com --subject "Hi" --text "Hello"
240
+ shipmail webhooks list
241
+ shipmail webhooks create --url https://example.com/hook --events message.received,message.bounced
242
+ shipmail suppressions list
243
+ shipmail suppressions remove <email>
244
+
245
+ Global options:
246
+ --api-key <key> API key. Defaults to SHIPMAIL_API_KEY.
247
+ --base-url <url> API base URL. Defaults to SHIPMAIL_BASE_URL or ${DEFAULT_BASE_URL}.
248
+ --header "K: V" Extra header for the generic api command. Repeatable.
249
+
250
+ Environment:
251
+ SHIPMAIL_API_KEY API key for authenticated commands.
252
+ SHIPMAIL_BASE_URL Optional API base URL.
253
+ `;
254
+ }
255
+ function requirePositional(positional, index, label) {
256
+ const value = positional[index];
257
+ if (!value) throw new CliUsageError(`Missing ${label}.`);
258
+ return value;
259
+ }
260
+ function inboxListParams(options) {
261
+ const params = {};
262
+ const folderId = getString(options, "folder-id");
263
+ const folderRole = getString(options, "folder-role");
264
+ const searchText = getString(options, "search");
265
+ const position = getNumber(options, "position");
266
+ const limit = getNumber(options, "limit");
267
+ const hasKeyword = getString(options, "has-keyword");
268
+ const notKeyword = getString(options, "not-keyword");
269
+ if (folderId) params.folder_id = folderId;
270
+ if (folderRole) params.folder_role = folderRole;
271
+ if (searchText) params.search_text = searchText;
272
+ if (position !== void 0) params.position = position;
273
+ if (limit !== void 0) params.limit = limit;
274
+ if (hasKeyword) params.has_keyword = hasKeyword;
275
+ if (notKeyword) params.not_keyword = notKeyword;
276
+ return params;
277
+ }
278
+ function messageRecipients(options, key) {
279
+ return getStringList(options, key);
280
+ }
281
+ function webhookEvents(options) {
282
+ const events = getStringList(options, "events");
283
+ if (events.length === 0) throw new CliUsageError("Provide at least one --events value.");
284
+ const invalid = events.filter((event) => !WEBHOOK_EVENT_TYPE_SET.has(event));
285
+ if (invalid.length > 0) {
286
+ throw new CliUsageError(`Invalid webhook event type: ${invalid.join(", ")}.`);
287
+ }
288
+ return events;
289
+ }
290
+ function webhookDeliveryParams(options) {
291
+ const status = getString(options, "status");
292
+ const eventType = getString(options, "event-type");
293
+ return {
294
+ cursor: getString(options, "cursor"),
295
+ limit: getNumber(options, "limit"),
296
+ ...status ? { status } : {},
297
+ ...eventType ? { event_type: eventType } : {}
298
+ };
299
+ }
300
+ function sendMessageParams(options) {
301
+ const mailboxId = getString(options, "mailbox-id");
302
+ const from = getString(options, "from");
303
+ const to = messageRecipients(options, "to");
304
+ const cc = messageRecipients(options, "cc");
305
+ const bcc = messageRecipients(options, "bcc");
306
+ const replyTo = getString(options, "reply-to");
307
+ const subject = getRequiredString(options, "subject");
308
+ const text = getString(options, "text");
309
+ const html = getString(options, "html");
310
+ if (!mailboxId && !from) throw new CliUsageError("Provide --mailbox-id or --from.");
311
+ if (to.length === 0) throw new CliUsageError("Provide at least one --to recipient.");
312
+ if (!text && !html) throw new CliUsageError("Provide --text or --html.");
313
+ return {
314
+ ...mailboxId ? { mailbox_id: mailboxId } : {},
315
+ ...from ? { from } : {},
316
+ to,
317
+ ...cc.length > 0 ? { cc } : {},
318
+ ...bcc.length > 0 ? { bcc } : {},
319
+ ...replyTo ? { reply_to: replyTo } : {},
320
+ subject,
321
+ ...html ? { html } : {},
322
+ ...text ? { text } : {}
323
+ };
324
+ }
325
+ async function runDomains(client, parsed, out) {
326
+ const subcommand = parsed.positional[1];
327
+ if (subcommand === "list") {
328
+ writeJson(
329
+ out,
330
+ await client.domains.list({
331
+ cursor: getString(parsed.options, "cursor"),
332
+ limit: getNumber(parsed.options, "limit")
333
+ })
334
+ );
335
+ return;
336
+ }
337
+ if (subcommand === "search") {
338
+ writeJson(
339
+ out,
340
+ await client.domains.search({ keyword: requirePositional(parsed.positional, 2, "keyword") })
341
+ );
342
+ return;
343
+ }
344
+ if (subcommand === "create") {
345
+ writeJson(
346
+ out,
347
+ await client.domains.create({ name: requirePositional(parsed.positional, 2, "domain name") })
348
+ );
349
+ return;
350
+ }
351
+ if (subcommand === "get") {
352
+ writeJson(out, await client.domains.get(requirePositional(parsed.positional, 2, "domain id")));
353
+ return;
354
+ }
355
+ if (subcommand === "verify") {
356
+ writeJson(
357
+ out,
358
+ await client.domains.verify(requirePositional(parsed.positional, 2, "domain id"))
359
+ );
360
+ return;
361
+ }
362
+ if (subcommand === "delete") {
363
+ const id = requirePositional(parsed.positional, 2, "domain id");
364
+ await client.domains.delete(id);
365
+ writeJson(out, { ok: true, id });
366
+ return;
367
+ }
368
+ throw new CliUsageError("Unknown domains command.");
369
+ }
370
+ async function runMailboxes(client, parsed, out) {
371
+ const subcommand = parsed.positional[1];
372
+ if (subcommand === "list") {
373
+ writeJson(
374
+ out,
375
+ await client.mailboxes.list({
376
+ domain_id: getString(parsed.options, "domain-id"),
377
+ cursor: getString(parsed.options, "cursor"),
378
+ limit: getNumber(parsed.options, "limit")
379
+ })
380
+ );
381
+ return;
382
+ }
383
+ if (subcommand === "create") {
384
+ const displayName = getString(parsed.options, "display-name");
385
+ writeJson(
386
+ out,
387
+ await client.mailboxes.create({
388
+ domain_id: getRequiredString(parsed.options, "domain-id"),
389
+ address: getRequiredString(parsed.options, "address"),
390
+ ...displayName ? { display_name: displayName } : {}
391
+ })
392
+ );
393
+ return;
394
+ }
395
+ if (subcommand === "get") {
396
+ writeJson(
397
+ out,
398
+ await client.mailboxes.get(requirePositional(parsed.positional, 2, "mailbox id"))
399
+ );
400
+ return;
401
+ }
402
+ if (subcommand === "folders") {
403
+ writeJson(
404
+ out,
405
+ await client.mailboxes.listFolders(requirePositional(parsed.positional, 2, "mailbox id"))
406
+ );
407
+ return;
408
+ }
409
+ if (subcommand === "identities") {
410
+ writeJson(
411
+ out,
412
+ await client.mailboxes.listIdentities(requirePositional(parsed.positional, 2, "mailbox id"))
413
+ );
414
+ return;
415
+ }
416
+ if (subcommand === "rules") {
417
+ writeJson(
418
+ out,
419
+ await client.mailboxes.getRules(requirePositional(parsed.positional, 2, "mailbox id"))
420
+ );
421
+ return;
422
+ }
423
+ throw new CliUsageError("Unknown mailboxes command.");
424
+ }
425
+ async function runInbox(client, parsed, out) {
426
+ const subcommand = parsed.positional[1];
427
+ if (subcommand === "list") {
428
+ writeJson(
429
+ out,
430
+ await client.mailboxes.listInboxMessages(
431
+ requirePositional(parsed.positional, 2, "mailbox id"),
432
+ inboxListParams(parsed.options)
433
+ )
434
+ );
435
+ return;
436
+ }
437
+ if (subcommand === "thread") {
438
+ writeJson(
439
+ out,
440
+ await client.mailboxes.getInboxThread(
441
+ requirePositional(parsed.positional, 2, "mailbox id"),
442
+ requirePositional(parsed.positional, 3, "thread id")
443
+ )
444
+ );
445
+ return;
446
+ }
447
+ if (subcommand === "update") {
448
+ const read = getBoolean(parsed.options, "read");
449
+ const starred = getBoolean(parsed.options, "starred");
450
+ if (read === void 0 && starred === void 0) {
451
+ throw new CliUsageError("Provide --read or --starred.");
452
+ }
453
+ writeJson(
454
+ out,
455
+ await client.mailboxes.updateInboxMessage(
456
+ requirePositional(parsed.positional, 2, "mailbox id"),
457
+ requirePositional(parsed.positional, 3, "message id"),
458
+ {
459
+ ...read !== void 0 ? { read } : {},
460
+ ...starred !== void 0 ? { starred } : {}
461
+ }
462
+ )
463
+ );
464
+ return;
465
+ }
466
+ if (subcommand === "move") {
467
+ const targetRole = getString(parsed.options, "target-role");
468
+ const targetFolderId = getString(parsed.options, "target-folder-id");
469
+ if (Boolean(targetRole) === Boolean(targetFolderId)) {
470
+ throw new CliUsageError("Provide exactly one of --target-role or --target-folder-id.");
471
+ }
472
+ writeJson(
473
+ out,
474
+ await client.mailboxes.moveInboxMessage(
475
+ requirePositional(parsed.positional, 2, "mailbox id"),
476
+ requirePositional(parsed.positional, 3, "message id"),
477
+ {
478
+ ...getString(parsed.options, "from-folder-id") ? { from_folder_id: getString(parsed.options, "from-folder-id") } : {},
479
+ ...targetRole ? { target_role: targetRole } : {},
480
+ ...targetFolderId ? { target_folder_id: targetFolderId } : {}
481
+ }
482
+ )
483
+ );
484
+ return;
485
+ }
486
+ throw new CliUsageError("Unknown inbox command.");
487
+ }
488
+ async function runMessages(client, parsed, out) {
489
+ const subcommand = parsed.positional[1];
490
+ if (subcommand === "send") {
491
+ writeJson(out, await client.messages.send(sendMessageParams(parsed.options)));
492
+ return;
493
+ }
494
+ if (subcommand === "list") {
495
+ writeJson(
496
+ out,
497
+ await client.messages.list({
498
+ mailbox_id: getRequiredString(parsed.options, "mailbox-id"),
499
+ cursor: getString(parsed.options, "cursor"),
500
+ limit: getNumber(parsed.options, "limit")
501
+ })
502
+ );
503
+ return;
504
+ }
505
+ if (subcommand === "get") {
506
+ writeJson(
507
+ out,
508
+ await client.messages.get(requirePositional(parsed.positional, 2, "message id"))
509
+ );
510
+ return;
511
+ }
512
+ throw new CliUsageError("Unknown messages command.");
513
+ }
514
+ async function runWebhooks(client, parsed, out) {
515
+ const subcommand = parsed.positional[1];
516
+ if (subcommand === "list") {
517
+ writeJson(
518
+ out,
519
+ await client.webhooks.list({
520
+ cursor: getString(parsed.options, "cursor"),
521
+ limit: getNumber(parsed.options, "limit")
522
+ })
523
+ );
524
+ return;
525
+ }
526
+ if (subcommand === "create") {
527
+ const description = getString(parsed.options, "description");
528
+ writeJson(
529
+ out,
530
+ await client.webhooks.create({
531
+ url: getRequiredString(parsed.options, "url"),
532
+ events: webhookEvents(parsed.options),
533
+ ...description ? { description } : {}
534
+ })
535
+ );
536
+ return;
537
+ }
538
+ if (subcommand === "test") {
539
+ writeJson(
540
+ out,
541
+ await client.webhooks.test(requirePositional(parsed.positional, 2, "webhook id"))
542
+ );
543
+ return;
544
+ }
545
+ if (subcommand === "deliveries") {
546
+ writeJson(
547
+ out,
548
+ await client.webhooks.listDeliveries(
549
+ requirePositional(parsed.positional, 2, "webhook id"),
550
+ webhookDeliveryParams(parsed.options)
551
+ )
552
+ );
553
+ return;
554
+ }
555
+ throw new CliUsageError("Unknown webhooks command.");
556
+ }
557
+ async function runSuppressions(client, parsed, out) {
558
+ const subcommand = parsed.positional[1];
559
+ if (subcommand === "list") {
560
+ writeJson(
561
+ out,
562
+ await client.suppressions.list({
563
+ cursor: getString(parsed.options, "cursor"),
564
+ limit: getNumber(parsed.options, "limit")
565
+ })
566
+ );
567
+ return;
568
+ }
569
+ if (subcommand === "remove") {
570
+ const email = requirePositional(parsed.positional, 2, "email");
571
+ await client.suppressions.remove(email);
572
+ writeJson(out, { ok: true, email });
573
+ return;
574
+ }
575
+ throw new CliUsageError("Unknown suppressions command.");
576
+ }
577
+ function writeError(err, errOut) {
578
+ if (err instanceof CliUsageError) {
579
+ errOut.write(`Error: ${err.message}
580
+
581
+ ${usage()}`);
582
+ return;
583
+ }
584
+ if (err instanceof CliHttpError) {
585
+ errOut.write(`Error: HTTP ${err.status}
586
+ ${JSON.stringify(err.body, null, 2)}
587
+ `);
588
+ return;
589
+ }
590
+ if (err instanceof ShipMailError) {
591
+ const status = err.status !== void 0 ? ` (HTTP ${err.status})` : "";
592
+ const requestId = err.requestId ? ` request_id=${err.requestId}` : "";
593
+ errOut.write(`Error: ${err.message}${status}${requestId}
594
+ `);
595
+ return;
596
+ }
597
+ errOut.write(`Error: ${err instanceof Error ? err.message : String(err)}
598
+ `);
599
+ }
600
+ async function runCli(argv, options = {}) {
601
+ const parsed = parseCliArgs(argv);
602
+ const out = options.stdout ?? stdout;
603
+ const errOut = options.stderr ?? stderr;
604
+ const command = parsed.positional[0];
605
+ try {
606
+ if (command === "version" || optionValue(parsed.options, "version")) {
607
+ out.write(`${VERSION}
608
+ `);
609
+ return 0;
610
+ }
611
+ if (!command || command === "help" || command === "-h" || optionValue(parsed.options, "help")) {
612
+ out.write(usage());
613
+ return 0;
614
+ }
615
+ if (command === "status") {
616
+ const response = await rawApiRequest(parsed, options, "GET", "/status", void 0, false);
617
+ await writeRawResponse(parsed, { stdout: out }, response);
618
+ return 0;
619
+ }
620
+ if (command === "api") {
621
+ const method = requirePositional(parsed.positional, 1, "method").toUpperCase();
622
+ const path = apiPath(requirePositional(parsed.positional, 2, "path"));
623
+ const body = await readJsonInput(getString(parsed.options, "data"));
624
+ const response = await rawApiRequest(parsed, options, method, path, body, path !== "/status");
625
+ await writeRawResponse(parsed, { stdout: out }, response);
626
+ return 0;
627
+ }
628
+ const client = createClient(parsed, options);
629
+ if (command === "domains") await runDomains(client, parsed, out);
630
+ else if (command === "mailboxes") await runMailboxes(client, parsed, out);
631
+ else if (command === "inbox") await runInbox(client, parsed, out);
632
+ else if (command === "messages") await runMessages(client, parsed, out);
633
+ else if (command === "webhooks") await runWebhooks(client, parsed, out);
634
+ else if (command === "suppressions") await runSuppressions(client, parsed, out);
635
+ else throw new CliUsageError(`Unknown command: ${command}.`);
636
+ return 0;
637
+ } catch (err) {
638
+ writeError(err, errOut);
639
+ return 1;
640
+ }
641
+ }
642
+ async function main() {
643
+ const code = await runCli(process.argv.slice(2));
644
+ exit(code);
645
+ }
646
+ if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
647
+ void main();
648
+ }
649
+ export {
650
+ runCli
651
+ };
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "shipmail-cli",
3
+ "version": "0.1.0",
4
+ "description": "Official CLI for the ShipMail API",
5
+ "type": "module",
6
+ "bin": {
7
+ "shipmail": "dist/cli.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsup src/cli.ts --format esm --dts --clean",
16
+ "dev": "tsx src/cli.ts",
17
+ "prepack": "bun run build",
18
+ "test": "bun test",
19
+ "typecheck": "tsc --noEmit"
20
+ },
21
+ "keywords": [
22
+ "shipmail",
23
+ "email",
24
+ "cli",
25
+ "email-api",
26
+ "domain",
27
+ "terminal"
28
+ ],
29
+ "author": "ShipMail",
30
+ "license": "MIT",
31
+ "homepage": "https://shipmail.to/docs/cli",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/jcoulaud/ShipMail.git",
35
+ "directory": "packages/shipmail-cli"
36
+ },
37
+ "engines": {
38
+ "node": ">=18"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "dependencies": {
44
+ "shipmail": "0.1.21"
45
+ },
46
+ "devDependencies": {
47
+ "tsup": "^8.4.0",
48
+ "tsx": "^4.19.0",
49
+ "typescript": "^5.8.3"
50
+ }
51
+ }