@develemail/cli 0.1.6 → 0.1.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 +43 -0
  2. package/dist/index.js +387 -42
  3. package/package.json +2 -2
package/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # @develemail/cli
2
+
3
+ The `devel` command-line tool for DevelEmail: manage projects, domains and API
4
+ keys, publish DNS, and send test emails.
5
+
6
+ ```bash
7
+ npm install -g @develemail/cli
8
+ devel --version
9
+ ```
10
+
11
+ ## Onboarding
12
+
13
+ ```bash
14
+ devel login # opens a browser; saves an owner session
15
+ devel domains add example.com # prints the domain ID
16
+ devel domains records <id> # DNS records to publish (--format bind)
17
+ devel domains publish-dns <id> # or publish them to Cloudflare for you
18
+ devel domains verify <id> --wait # poll until DNS verifies
19
+ devel init --project my-app --from noreply@example.com
20
+ ```
21
+
22
+ `devel login` defaults to `--url https://develemail.com/api`. The session is
23
+ saved to `~/.config/develemail/auth.json`.
24
+
25
+ `publish-dns` needs a Cloudflare token connected first:
26
+ `devel cloudflare connect --token-env CLOUDFLARE_API_TOKEN`.
27
+
28
+ ## Receiving mail
29
+
30
+ ```bash
31
+ devel inbound add example.com privacy --if-missing # inbox-only mailbox
32
+ devel inbound list example.com
33
+ devel inbound check example.com # does the MX reach us?
34
+ ```
35
+
36
+ `devel init` creates the project and an API key and writes
37
+ `DEVELEMAIL_BASE_URL` and `DEVELEMAIL_API_KEY` to your env files. After that,
38
+ `devel send`, `devel logs` and `devel tail` use that API key.
39
+
40
+ ## Reference
41
+
42
+ Every command and option is documented in the
43
+ [CLI reference](https://docs.develemail.com/cli-reference) on the DevelEmail docs site.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/index.ts
4
- import { Command as Command11 } from "commander";
3
+ // src/program.ts
4
+ import { Command as Command13 } from "commander";
5
5
 
6
6
  // src/commands/send.ts
7
7
  import { Command } from "commander";
@@ -210,15 +210,46 @@ function openBrowser(url, deps = {}) {
210
210
  return true;
211
211
  }
212
212
 
213
+ // src/resolve-web-app-url.ts
214
+ var DISCOVERY_TIMEOUT_MS = 3e3;
215
+ function trimSlash(url) {
216
+ return url.replace(/\/+$/, "");
217
+ }
218
+ function legacyWebAppUrl(apiUrl) {
219
+ return trimSlash(apiUrl).replace(/\/api$/, "");
220
+ }
221
+ async function resolveWebAppUrl(apiUrl, fetchImpl = fetch, options = {}) {
222
+ if (options.appUrl?.trim()) return trimSlash(options.appUrl.trim());
223
+ const base = trimSlash(apiUrl);
224
+ try {
225
+ const res = await fetchImpl(`${base}/meta/public-urls`, {
226
+ signal: AbortSignal.timeout(options.timeoutMs ?? DISCOVERY_TIMEOUT_MS)
227
+ });
228
+ if (!res.ok) return legacyWebAppUrl(base);
229
+ const body = await res.json();
230
+ return typeof body.webAppUrl === "string" && body.webAppUrl.trim() ? trimSlash(body.webAppUrl.trim()) : legacyWebAppUrl(base);
231
+ } catch {
232
+ return legacyWebAppUrl(base);
233
+ }
234
+ }
235
+
213
236
  // src/commands/login.ts
214
- var loginCommand = new Command4("login").description("Authenticate with your DevelEmail instance via browser").option(
215
- "--url <url>",
216
- "Base URL of your DevelEmail API",
217
- "https://develemail.com/api"
237
+ var DEFAULT_API_URL = "https://develemail.com/api";
238
+ var loginCommand = new Command4("login").description("Authenticate with your DevelEmail instance via browser").option("--url <url>", "Base URL of your DevelEmail API", DEFAULT_API_URL).option(
239
+ "--app-url <url>",
240
+ "Dashboard URL to open for sign-in (discovered from the API by default)"
218
241
  ).action(async (opts) => {
242
+ if (!isHttpUrl(opts.url) || opts.appUrl && !isHttpUrl(opts.appUrl)) {
243
+ console.error("--url must be an http(s) URL");
244
+ process.exit(1);
245
+ return;
246
+ }
219
247
  const apiUrl = opts.url.replace(/\/$/, "");
220
- const baseUrl = apiUrl.replace(/\/api$/, "");
248
+ const webAppUrl = await resolveWebAppUrl(apiUrl, fetch, {
249
+ appUrl: opts.appUrl
250
+ });
221
251
  await new Promise((resolve2, reject) => {
252
+ let timeout;
222
253
  const server = createServer(
223
254
  (req, res) => {
224
255
  const url = new URL(req.url ?? "/", `http://localhost`);
@@ -240,6 +271,7 @@ var loginCommand = new Command4("login").description("Authenticate with your Dev
240
271
  );
241
272
  console.log("Authenticated successfully.");
242
273
  console.log("Session saved to ~/.config/develemail/auth.json");
274
+ clearTimeout(timeout);
243
275
  server.close();
244
276
  resolve2();
245
277
  }
@@ -251,13 +283,16 @@ var loginCommand = new Command4("login").description("Authenticate with your Dev
251
283
  return;
252
284
  }
253
285
  const port = addr.port;
254
- const authUrl = `${baseUrl}/cli-auth?port=${port}`;
286
+ const authUrl = `${webAppUrl}/cli-auth?port=${port}`;
255
287
  console.log("Opening browser for authentication...");
256
288
  console.log(`If the browser doesn't open, visit: ${authUrl}`);
257
289
  openBrowser(authUrl);
258
290
  });
259
- server.on("error", reject);
260
- setTimeout(
291
+ server.on("error", (err) => {
292
+ clearTimeout(timeout);
293
+ reject(err);
294
+ });
295
+ timeout = setTimeout(
261
296
  () => {
262
297
  server.close();
263
298
  reject(new Error("Authentication timed out after 5 minutes"));
@@ -294,6 +329,21 @@ var COL_ENV = 12;
294
329
  function col2(s, w) {
295
330
  return (s ?? "").slice(0, w).padEnd(w);
296
331
  }
332
+ function formatCooldown(minutes) {
333
+ if (minutes == null) return "platform default";
334
+ return minutes === 0 ? "disabled" : `${minutes} min`;
335
+ }
336
+ function parseCooldownMinutes(raw) {
337
+ if (raw === "default") return null;
338
+ const n = Number(raw);
339
+ if (!Number.isInteger(n) || n < 0 || n > 1440) {
340
+ console.error(
341
+ 'Error: --recipient-cooldown must be 0-1440 minutes, or "default"'
342
+ );
343
+ process.exit(1);
344
+ }
345
+ return n;
346
+ }
297
347
  var listCommand = new Command5("list").description("List all projects").action(async () => {
298
348
  const { url, token } = loadOwnerSession();
299
349
  const projects = await apiRequest(`${url}/projects`, token);
@@ -320,6 +370,9 @@ var getCommand = new Command5("get").description("Get project details").argument
320
370
  console.log(
321
371
  `Rate Limit: ${project.rateLimitPerDay ?? "Unlimited"} per day`
322
372
  );
373
+ console.log(
374
+ `Recipient Cooldown: ${formatCooldown(project.recipientCooldownMinutes)}`
375
+ );
323
376
  if (project.domainId) {
324
377
  console.log(`Domain ID: ${project.domainId}`);
325
378
  }
@@ -346,10 +399,33 @@ var createCommand = new Command5("create").description("Create a new project").r
346
399
  console.log(`Created project: ${project.name} (${project.id})`);
347
400
  }
348
401
  );
349
- var projectCommand = new Command5("project").description("Manage projects").addCommand(listCommand).addCommand(getCommand).addCommand(createCommand);
402
+ var updateCommand = new Command5("update").description("Update project settings").argument("<id>", "Project ID").option(
403
+ "--recipient-cooldown <minutes>",
404
+ 'Minutes before the same subject can be re-sent to a recipient (0 disables, "default" restores the platform default)'
405
+ ).action(async (id, opts) => {
406
+ const body = {};
407
+ if (opts.recipientCooldown !== void 0) {
408
+ body.recipientCooldownMinutes = parseCooldownMinutes(
409
+ opts.recipientCooldown
410
+ );
411
+ }
412
+ if (Object.keys(body).length === 0) {
413
+ console.error("Error: nothing to update; pass --recipient-cooldown");
414
+ process.exit(1);
415
+ }
416
+ const { url, token } = loadOwnerSession();
417
+ const project = await apiRequest(`${url}/projects/${id}`, token, {
418
+ method: "PATCH",
419
+ body: JSON.stringify(body)
420
+ });
421
+ console.log(
422
+ `Updated project: ${project.name} (recipient cooldown: ${formatCooldown(project.recipientCooldownMinutes)})`
423
+ );
424
+ });
425
+ var projectCommand = new Command5("project").description("Manage projects").addCommand(listCommand).addCommand(getCommand).addCommand(createCommand).addCommand(updateCommand);
350
426
 
351
427
  // src/commands/domains.ts
352
- import { Command as Command7 } from "commander";
428
+ import { Command as Command8 } from "commander";
353
429
 
354
430
  // src/owner-api.ts
355
431
  var OwnerApiError = class extends Error {
@@ -365,7 +441,7 @@ async function ownerRequest(path, init) {
365
441
  ...init,
366
442
  headers: {
367
443
  Cookie: `session=${token}`,
368
- "Content-Type": "application/json",
444
+ ...init?.body != null ? { "Content-Type": "application/json" } : {},
369
445
  ...init?.headers ?? {}
370
446
  }
371
447
  });
@@ -379,6 +455,7 @@ async function ownerRequest(path, init) {
379
455
  const body = await res.json().catch(() => ({}));
380
456
  throw new OwnerApiError(body.message ?? res.statusText, res.status);
381
457
  }
458
+ if (res.status === 204) return void 0;
382
459
  return res.json();
383
460
  }
384
461
  function exitOnError(fn) {
@@ -481,6 +558,63 @@ var publishDnsCommand = new Command6("publish-dns").description("Publish a domai
481
558
  })
482
559
  );
483
560
 
561
+ // src/commands/domains-warmup.ts
562
+ import { Command as Command7 } from "commander";
563
+ var FULLY_WARMED_STAGE = 7;
564
+ var STAGE_LIMITS = {
565
+ 1: "50",
566
+ 2: "100",
567
+ 3: "200",
568
+ 4: "400",
569
+ 5: "800",
570
+ 6: "1,600",
571
+ 7: "Unlimited"
572
+ };
573
+ function printWarmupStatus(d) {
574
+ console.log(`Domain: ${d.name}`);
575
+ if (d.warmupStage === null) {
576
+ console.log("Warmup: not started");
577
+ return;
578
+ }
579
+ console.log(
580
+ d.warmupStage >= FULLY_WARMED_STAGE ? "Stage: Fully warmed" : `Stage: ${d.warmupStage} of ${FULLY_WARMED_STAGE}`
581
+ );
582
+ console.log(`Daily limit: ${STAGE_LIMITS[d.warmupStage] ?? "Unknown"}`);
583
+ if (d.warmupPausedAt) {
584
+ console.log(`Paused since ${new Date(d.warmupPausedAt).toLocaleString()}`);
585
+ }
586
+ }
587
+ async function runWarmup(id, opts) {
588
+ if (opts.pause && opts.resume) {
589
+ throw new Error("Use either --pause or --resume, not both.");
590
+ }
591
+ const patch = (warmupPausedAt) => ownerRequest(`/domains/${id}`, {
592
+ method: "PATCH",
593
+ body: JSON.stringify({ warmupPausedAt })
594
+ });
595
+ let domain;
596
+ if (opts.pause) {
597
+ domain = await patch((/* @__PURE__ */ new Date()).toISOString());
598
+ } else if (opts.resume) {
599
+ domain = await patch(null);
600
+ } else {
601
+ domain = await ownerRequest(`/domains/${id}`);
602
+ if (domain.warmupStage === null) {
603
+ domain = await ownerRequest(
604
+ `/domains/${id}/warmup/enable`,
605
+ {
606
+ method: "POST"
607
+ }
608
+ );
609
+ } else {
610
+ console.log("Warmup already in progress");
611
+ }
612
+ }
613
+ if (opts.json) console.log(JSON.stringify(domain, null, 2));
614
+ else printWarmupStatus(domain);
615
+ }
616
+ var warmupCommand = new Command7("warmup").description("Start, pause or resume domain warmup").argument("<id>", "Domain ID").option("--pause", "Pause the warmup ramp").option("--resume", "Resume a paused warmup ramp").option("--json", "Output the domain as JSON").action(exitOnError(runWarmup));
617
+
484
618
  // src/commands/domains.ts
485
619
  var COL_ID3 = 40;
486
620
  var COL_NAME2 = 30;
@@ -493,7 +627,7 @@ function printStatuses(d) {
493
627
  console.log(`DKIM Status: ${d.dkimStatus ?? "\u2014"}`);
494
628
  console.log(`DMARC Status: ${d.dmarcStatus ?? "\u2014"}`);
495
629
  }
496
- var listCommand2 = new Command7("list").description("List all domains").action(
630
+ var listCommand2 = new Command8("list").description("List all domains").action(
497
631
  exitOnError(async () => {
498
632
  const domains = await ownerRequest("/domains");
499
633
  if (domains.length === 0) {
@@ -511,7 +645,7 @@ var listCommand2 = new Command7("list").description("List all domains").action(
511
645
  }
512
646
  })
513
647
  );
514
- var getCommand2 = new Command7("get").description("Get domain details").argument("<id>", "Domain ID").action(
648
+ var getCommand2 = new Command8("get").description("Get domain details").argument("<id>", "Domain ID").action(
515
649
  exitOnError(async (id) => {
516
650
  const domain = await ownerRequest(`/domains/${id}`);
517
651
  console.log(`ID: ${domain.id}`);
@@ -527,7 +661,7 @@ ${domain.dkimPublicKey}`);
527
661
  console.log(`Created At: ${new Date(domain.createdAt).toISOString()}`);
528
662
  })
529
663
  );
530
- var addCommand = new Command7("add").description("Create a domain and print its required DNS records").argument("<name>", "Domain name, e.g. example.com").option("--dkim-selector <selector>", "DKIM selector").option("--json", "Output JSON").action(
664
+ var addCommand = new Command8("add").description("Create a domain and print its required DNS records").argument("<name>", "Domain name, e.g. example.com").option("--dkim-selector <selector>", "DKIM selector").option("--json", "Output JSON").action(
531
665
  exitOnError(
532
666
  async (name, opts) => {
533
667
  const domain = await ownerRequest("/domains", {
@@ -547,7 +681,7 @@ var addCommand = new Command7("add").description("Create a domain and print its
547
681
  }
548
682
  )
549
683
  );
550
- var recordsCommand = new Command7("records").description("Print the DNS records a domain requires").argument("<id>", "Domain ID").option("--json", "Output JSON").option("--format <format>", "table | bind", "table").action(
684
+ var recordsCommand = new Command8("records").description("Print the DNS records a domain requires").argument("<id>", "Domain ID").option("--json", "Output JSON").option("--format <format>", "table | bind", "table").action(
551
685
  exitOnError(
552
686
  async (id, opts) => {
553
687
  const records = await ownerRequest(
@@ -559,7 +693,7 @@ var recordsCommand = new Command7("records").description("Print the DNS records
559
693
  );
560
694
  var POLL_INTERVAL_MS2 = 1e4;
561
695
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
562
- var verifyCommand = new Command7("verify").description("Trigger domain verification").argument("<id>", "Domain ID").option("--wait", "Poll until verified or timed out").option("--timeout <seconds>", "Max seconds to wait with --wait", "600").action(
696
+ var verifyCommand = new Command8("verify").description("Trigger domain verification").argument("<id>", "Domain ID").option("--wait", "Poll until verified or timed out").option("--timeout <seconds>", "Max seconds to wait with --wait", "600").action(
563
697
  exitOnError(
564
698
  async (id, opts) => {
565
699
  const deadline = Date.now() + Number(opts.timeout) * 1e3;
@@ -582,11 +716,209 @@ var verifyCommand = new Command7("verify").description("Trigger domain verificat
582
716
  }
583
717
  )
584
718
  );
585
- var domainsCommand = new Command7("domains").description("Manage domains").addCommand(listCommand2).addCommand(getCommand2).addCommand(addCommand).addCommand(recordsCommand).addCommand(publishDnsCommand).addCommand(verifyCommand);
719
+ var domainsCommand = new Command8("domains").description("Manage domains").addCommand(listCommand2).addCommand(getCommand2).addCommand(addCommand).addCommand(recordsCommand).addCommand(publishDnsCommand).addCommand(verifyCommand).addCommand(warmupCommand);
720
+
721
+ // src/commands/inbound.ts
722
+ import { createInterface as createInterface2 } from "readline/promises";
723
+ import { Command as Command9 } from "commander";
724
+
725
+ // src/commands/inbound-check.ts
726
+ import { resolve4, resolveMx } from "dns/promises";
727
+ var INBOUND_HOST = "mail.develemail.com";
728
+ async function checkMx(domain) {
729
+ const [mx, inbound] = await Promise.all([
730
+ resolveMx(domain),
731
+ resolve4(INBOUND_HOST)
732
+ ]);
733
+ const checks = await Promise.all(
734
+ mx.map(async (r) => {
735
+ const addresses = await resolve4(r.exchange).catch(() => []);
736
+ return {
737
+ exchange: r.exchange,
738
+ priority: r.priority,
739
+ addresses,
740
+ pointsAtInbound: addresses.some((a) => inbound.includes(a))
741
+ };
742
+ })
743
+ );
744
+ return checks.sort((a, b) => a.priority - b.priority);
745
+ }
746
+
747
+ // src/commands/inbound.ts
748
+ var COL_ADDRESS = 32;
749
+ var COL_ACTIVE = 8;
750
+ var COL_WEBHOOK = 36;
751
+ var COL_NOTIFY = 30;
752
+ function col4(s, w) {
753
+ return s.slice(0, w).padEnd(w);
754
+ }
755
+ function toAddress(localPart) {
756
+ return localPart === "*" ? null : localPart;
757
+ }
758
+ function label(address) {
759
+ return address ?? "*";
760
+ }
761
+ async function resolveDomain(nameOrId) {
762
+ const domains = await ownerRequest("/domains");
763
+ const match = domains.find(
764
+ (d) => d.id === nameOrId || d.name === nameOrId.toLowerCase()
765
+ );
766
+ if (!match) throw new Error(`Domain not found: ${nameOrId}`);
767
+ return match;
768
+ }
769
+ async function findRoute(domain, localPart) {
770
+ const address = toAddress(localPart.toLowerCase());
771
+ const routes = await ownerRequest(
772
+ `/domains/${domain.id}/inbound-routes`
773
+ );
774
+ const route = routes.find((r) => r.address === address);
775
+ if (!route) {
776
+ throw new Error(`No inbound route for ${label(address)}@${domain.name}`);
777
+ }
778
+ return route;
779
+ }
780
+ function noneToNull(value) {
781
+ return value === "none" ? null : value;
782
+ }
783
+ async function confirm2(question) {
784
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
785
+ try {
786
+ return /^y(es)?$/i.test((await rl.question(question)).trim());
787
+ } finally {
788
+ rl.close();
789
+ }
790
+ }
791
+ var listCommand3 = new Command9("list").description("List inbound routes for a domain").argument("<domain>", "Domain name or ID").option("--json", "Output JSON").action(
792
+ exitOnError(async (domainArg, opts) => {
793
+ const domain = await resolveDomain(domainArg);
794
+ const routes = await ownerRequest(
795
+ `/domains/${domain.id}/inbound-routes`
796
+ );
797
+ if (opts.json) {
798
+ console.log(JSON.stringify(routes, null, 2));
799
+ return;
800
+ }
801
+ if (routes.length === 0) {
802
+ console.log(`No inbound routes for ${domain.name}.`);
803
+ return;
804
+ }
805
+ const header = `${col4("ADDRESS", COL_ADDRESS)} ${col4("ACTIVE", COL_ACTIVE)} ${col4("WEBHOOK", COL_WEBHOOK)} ${col4("NOTIFY", COL_NOTIFY)} CREATED`;
806
+ console.log(header);
807
+ console.log("-".repeat(header.length + 10));
808
+ for (const r of routes) {
809
+ console.log(
810
+ `${col4(`${label(r.address)}@${domain.name}`, COL_ADDRESS)} ${col4(r.active ? "yes" : "no", COL_ACTIVE)} ${col4(r.webhookUrl ?? "-", COL_WEBHOOK)} ${col4(r.notifyEmail ?? "-", COL_NOTIFY)} ${new Date(r.createdAt).toLocaleDateString()}`
811
+ );
812
+ }
813
+ })
814
+ );
815
+ var addCommand2 = new Command9("add").description("Create an inbound route (use * for a catch-all)").argument("<domain>", "Domain name or ID").argument("<local-part>", "Local part, or * for catch-all").option("--webhook <url>", "Deliver received mail to this webhook").option("--notify <email>", "Send an arrival notification to this address").option("--if-missing", "Exit 0 if the route already exists").action(
816
+ exitOnError(
817
+ async (domainArg, localPart, opts) => {
818
+ const domain = await resolveDomain(domainArg);
819
+ const address = toAddress(localPart);
820
+ try {
821
+ const route = await ownerRequest(
822
+ `/domains/${domain.id}/inbound-routes`,
823
+ {
824
+ method: "POST",
825
+ body: JSON.stringify({
826
+ address,
827
+ webhookUrl: opts.webhook ?? null,
828
+ notifyEmail: opts.notify ?? null
829
+ })
830
+ }
831
+ );
832
+ console.log(
833
+ `Created inbound route ${label(address)}@${domain.name} (${route.id})`
834
+ );
835
+ } catch (err) {
836
+ if (opts.ifMissing && err instanceof OwnerApiError && err.status === 409) {
837
+ console.log(
838
+ `Inbound route ${label(address)}@${domain.name} already exists`
839
+ );
840
+ return;
841
+ }
842
+ throw err;
843
+ }
844
+ }
845
+ )
846
+ );
847
+ var updateCommand2 = new Command9("update").description("Update an inbound route").argument("<domain>", "Domain name or ID").argument("<local-part>", "Local part, or * for catch-all").option("--webhook <url|none>", "Set or clear the webhook").option("--notify <email|none>", "Set or clear the notify email").option("--active", "Enable the route").option("--inactive", "Disable the route").action(
848
+ exitOnError(
849
+ async (domainArg, localPart, opts) => {
850
+ if (opts.active && opts.inactive) {
851
+ throw new Error("Pass only one of --active and --inactive");
852
+ }
853
+ const domain = await resolveDomain(domainArg);
854
+ const route = await findRoute(domain, localPart);
855
+ const body = {
856
+ webhookUrl: noneToNull(opts.webhook),
857
+ notifyEmail: noneToNull(opts.notify),
858
+ active: opts.active ? true : opts.inactive ? false : void 0
859
+ };
860
+ await ownerRequest(
861
+ `/domains/${domain.id}/inbound-routes/${route.id}`,
862
+ { method: "PATCH", body: JSON.stringify(body) }
863
+ );
864
+ console.log(`Updated ${label(route.address)}@${domain.name}`);
865
+ }
866
+ )
867
+ );
868
+ var removeCommand = new Command9("remove").description("Delete an inbound route").argument("<domain>", "Domain name or ID").argument("<local-part>", "Local part, or * for catch-all").option("--yes", "Delete without prompting").action(
869
+ exitOnError(
870
+ async (domainArg, localPart, opts) => {
871
+ const domain = await resolveDomain(domainArg);
872
+ const route = await findRoute(domain, localPart);
873
+ const target = `${label(route.address)}@${domain.name}`;
874
+ const ok = opts.yes || process.stdin.isTTY === true && await confirm2(`Delete inbound route ${target}? [y/N] `);
875
+ if (!ok) {
876
+ console.log("Nothing deleted. Re-run with --yes to delete.");
877
+ return;
878
+ }
879
+ await ownerRequest(
880
+ `/domains/${domain.id}/inbound-routes/${route.id}`,
881
+ { method: "DELETE" }
882
+ );
883
+ console.log(`Deleted ${target}`);
884
+ }
885
+ )
886
+ );
887
+ var checkCommand = new Command9("check").description("Show whether a domain MX points at the inbound server").argument("<domain>", "Domain name or ID").option("--json", "Output JSON").action(
888
+ exitOnError(async (domainArg, opts) => {
889
+ const ref = await resolveDomain(domainArg);
890
+ const [domain, mx] = await Promise.all([
891
+ ownerRequest(`/domains/${ref.id}`),
892
+ checkMx(ref.name)
893
+ ]);
894
+ const ok = mx.some((r) => r.pointsAtInbound);
895
+ if (opts.json) {
896
+ console.log(
897
+ JSON.stringify(
898
+ { mxStatus: domain.mxStatus ?? null, ok, mx },
899
+ null,
900
+ 2
901
+ )
902
+ );
903
+ return;
904
+ }
905
+ console.log(`MX Status: ${domain.mxStatus ?? "\u2014"}`);
906
+ if (mx.length === 0) console.log("No MX records found.");
907
+ for (const r of mx) {
908
+ console.log(
909
+ `MX ${r.priority} ${r.exchange} -> ${r.addresses.join(", ") || "unresolved"} ${r.pointsAtInbound ? "(inbound server)" : "(not the inbound server)"}`
910
+ );
911
+ }
912
+ console.log(
913
+ ok ? `OK: mail for ${ref.name} reaches ${INBOUND_HOST}.` : `Mail for ${ref.name} does not reach ${INBOUND_HOST}; point its MX at ${INBOUND_HOST}.`
914
+ );
915
+ })
916
+ );
917
+ var inboundCommand = new Command9("inbound").description("Manage inbound mailboxes").addCommand(listCommand3).addCommand(addCommand2).addCommand(updateCommand2).addCommand(removeCommand).addCommand(checkCommand);
586
918
 
587
919
  // src/commands/cloudflare.ts
588
- import { Command as Command8 } from "commander";
589
- var connectCommand = new Command8("connect").description("Connect a Cloudflare API token to your organization").requiredOption(
920
+ import { Command as Command10 } from "commander";
921
+ var connectCommand = new Command10("connect").description("Connect a Cloudflare API token to your organization").requiredOption(
590
922
  "--token-env <VAR>",
591
923
  "Environment variable holding the Cloudflare API token"
592
924
  ).action(
@@ -602,7 +934,7 @@ var connectCommand = new Command8("connect").description("Connect a Cloudflare A
602
934
  console.log("Cloudflare connected.");
603
935
  })
604
936
  );
605
- var statusCommand = new Command8("status").description("Show whether Cloudflare is connected").action(
937
+ var statusCommand = new Command10("status").description("Show whether Cloudflare is connected").action(
606
938
  exitOnError(async () => {
607
939
  const { connected } = await ownerRequest(
608
940
  "/cloudflare/status"
@@ -610,7 +942,7 @@ var statusCommand = new Command8("status").description("Show whether Cloudflare
610
942
  console.log(connected ? "Connected" : "Not connected");
611
943
  })
612
944
  );
613
- var zonesCommand = new Command8("zones").description("List Cloudflare zones").option("--json", "Output JSON").action(
945
+ var zonesCommand = new Command10("zones").description("List Cloudflare zones").option("--json", "Output JSON").action(
614
946
  exitOnError(async (opts) => {
615
947
  const zones = await ownerRequest("/cloudflare/zones");
616
948
  if (opts.json) {
@@ -620,10 +952,10 @@ var zonesCommand = new Command8("zones").description("List Cloudflare zones").op
620
952
  for (const z of zones) console.log(`${z.id} ${z.name} ${z.status}`);
621
953
  })
622
954
  );
623
- var cloudflareCommand = new Command8("cloudflare").description("Manage the Cloudflare integration").addCommand(connectCommand).addCommand(statusCommand).addCommand(zonesCommand);
955
+ var cloudflareCommand = new Command10("cloudflare").description("Manage the Cloudflare integration").addCommand(connectCommand).addCommand(statusCommand).addCommand(zonesCommand);
624
956
 
625
957
  // src/commands/key.ts
626
- import { Command as Command9 } from "commander";
958
+ import { Command as Command11 } from "commander";
627
959
  function ownerHeaders2(token) {
628
960
  return { Cookie: `session=${token}`, "Content-Type": "application/json" };
629
961
  }
@@ -639,7 +971,7 @@ async function apiRequest2(url, token, init) {
639
971
  }
640
972
  return res.json();
641
973
  }
642
- var listCommand3 = new Command9("list").description("List API keys for a project").requiredOption("--project <id>", "Project ID").action(async (opts) => {
974
+ var listCommand4 = new Command11("list").description("List API keys for a project").requiredOption("--project <id>", "Project ID").action(async (opts) => {
643
975
  const { url, token } = loadOwnerSession();
644
976
  const keys = await apiRequest2(
645
977
  `${url}/projects/${opts.project}/api-keys`,
@@ -654,7 +986,7 @@ var listCommand3 = new Command9("list").description("List API keys for a project
654
986
  console.log(`${status} ${k.keyPrefix}\u2026 ${k.name} (${k.id})`);
655
987
  }
656
988
  });
657
- var createCommand2 = new Command9("create").description("Create a new API key for a project").requiredOption("--project <id>", "Project ID").requiredOption("--name <name>", "Key name (e.g. production, ci)").option("--expires <date>", "Expiry date (ISO-8601, e.g. 2027-01-01)").option("--rate-limit <n>", "Max requests per minute").action(
989
+ var createCommand2 = new Command11("create").description("Create a new API key for a project").requiredOption("--project <id>", "Project ID").requiredOption("--name <name>", "Key name (e.g. production, ci)").option("--expires <date>", "Expiry date (ISO-8601, e.g. 2027-01-01)").option("--rate-limit <n>", "Max requests per minute").action(
658
990
  async (opts) => {
659
991
  const { url, token } = loadOwnerSession();
660
992
  const result = await apiRequest2(
@@ -676,10 +1008,10 @@ var createCommand2 = new Command9("create").description("Create a new API key fo
676
1008
  console.log("Store the secret now \u2014 it will not be shown again.");
677
1009
  }
678
1010
  );
679
- var keyCommand = new Command9("key").description("Manage API keys").addCommand(listCommand3).addCommand(createCommand2);
1011
+ var keyCommand = new Command11("key").description("Manage API keys").addCommand(listCommand4).addCommand(createCommand2);
680
1012
 
681
1013
  // src/commands/init.ts
682
- import { Command as Command10 } from "commander";
1014
+ import { Command as Command12 } from "commander";
683
1015
  import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
684
1016
  import { resolve } from "path";
685
1017
  function ownerHeaders3(token) {
@@ -709,7 +1041,7 @@ function upsertEnvFile(filePath, vars) {
709
1041
  const content = lines.join("\n").trimEnd() + (lines.some((l) => l.trim()) ? "\n" : "") + appended + "\n";
710
1042
  writeFileSync2(filePath, content, "utf-8");
711
1043
  }
712
- var initCommand = new Command10("init").description(
1044
+ var initCommand = new Command12("init").description(
713
1045
  "Provision a project + API key and write credentials to an env file"
714
1046
  ).requiredOption(
715
1047
  "--project <name>",
@@ -762,16 +1094,29 @@ var initCommand = new Command10("init").description(
762
1094
  }
763
1095
  );
764
1096
 
1097
+ // src/version.ts
1098
+ import { createRequire } from "module";
1099
+ var require2 = createRequire(import.meta.url);
1100
+ function readCliVersion() {
1101
+ return require2("../package.json").version;
1102
+ }
1103
+
1104
+ // src/program.ts
1105
+ function buildProgram() {
1106
+ const program = new Command13().name("devel").description("DevelEmail CLI \u2014 interact with your DevelEmail instance").version(readCliVersion());
1107
+ program.addCommand(loginCommand);
1108
+ program.addCommand(logoutCommand);
1109
+ program.addCommand(projectCommand);
1110
+ program.addCommand(domainsCommand);
1111
+ program.addCommand(inboundCommand);
1112
+ program.addCommand(cloudflareCommand);
1113
+ program.addCommand(keyCommand);
1114
+ program.addCommand(initCommand);
1115
+ program.addCommand(sendCommand);
1116
+ program.addCommand(logsCommand);
1117
+ program.addCommand(tailCommand);
1118
+ return program;
1119
+ }
1120
+
765
1121
  // src/index.ts
766
- var program = new Command11().name("devel").description("DevelEmail CLI \u2014 interact with your DevelEmail instance").version("0.1.0");
767
- program.addCommand(loginCommand);
768
- program.addCommand(logoutCommand);
769
- program.addCommand(projectCommand);
770
- program.addCommand(domainsCommand);
771
- program.addCommand(cloudflareCommand);
772
- program.addCommand(keyCommand);
773
- program.addCommand(initCommand);
774
- program.addCommand(sendCommand);
775
- program.addCommand(logsCommand);
776
- program.addCommand(tailCommand);
777
- program.parse(process.argv);
1122
+ buildProgram().parse(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@develemail/cli",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "devel": "dist/index.js"
@@ -16,7 +16,7 @@
16
16
  "dependencies": {
17
17
  "commander": "^12.1.0",
18
18
  "dotenv": "^16.4.5",
19
- "@develemail/sdk": "0.4.0"
19
+ "@develemail/sdk": "0.4.1"
20
20
  },
21
21
  "devDependencies": {
22
22
  "tsup": "^8.3.5",