@develemail/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 (2) hide show
  1. package/dist/index.js +342 -17
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command8 } from "commander";
4
+ import { Command as Command11 } from "commander";
5
5
 
6
6
  // src/commands/send.ts
7
7
  import { Command } from "commander";
@@ -91,7 +91,10 @@ var COL_STATUS = 10;
91
91
  function col(s, w) {
92
92
  return s.slice(0, w).padEnd(w);
93
93
  }
94
- var logsCommand = new Command2("logs").description("List recent emails").option("--limit <n>", "Number of emails to show", "20").option("--status <status>", "Filter by status (sent, queued, failed, \u2026)").action(async (opts) => {
94
+ var logsCommand = new Command2("logs").description("List recent emails").option("--limit <n>", "Number of emails to show", "20").option(
95
+ "--status <status>",
96
+ "Filter by status (sent, delivered, queued, deferred, bounced, failed, \u2026)"
97
+ ).action(async (opts) => {
95
98
  const cfg = loadConfig();
96
99
  const params = new URLSearchParams({ limit: opts.limit });
97
100
  if (opts.status) params.set("status", opts.status);
@@ -139,6 +142,12 @@ var tailCommand = new Command3("tail").description("Poll and print new email eve
139
142
  const cfg = loadConfig();
140
143
  const seen = /* @__PURE__ */ new Set();
141
144
  const startedAt = opts.since ? new Date(opts.since) : /* @__PURE__ */ new Date();
145
+ if (opts.since && isNaN(startedAt.getTime())) {
146
+ console.error(
147
+ `Error: invalid --since value "${opts.since}" \u2014 use ISO 8601 (e.g. 2026-07-20) or other valid date format`
148
+ );
149
+ process.exit(1);
150
+ }
142
151
  console.log("Tailing email events\u2026 (Ctrl-C to stop)");
143
152
  const poll = async () => {
144
153
  try {
@@ -173,15 +182,39 @@ import { Command as Command4 } from "commander";
173
182
  import {
174
183
  createServer
175
184
  } from "http";
176
- import { exec } from "child_process";
185
+
186
+ // src/open-browser.ts
187
+ import { execFile } from "child_process";
177
188
  import { platform } from "os";
178
- function openBrowser(url) {
179
- const cmd = platform() === "darwin" ? "open" : platform() === "win32" ? "start" : "xdg-open";
180
- exec(`${cmd} "${url}"`);
189
+ function isHttpUrl(url) {
190
+ try {
191
+ const { protocol } = new URL(url);
192
+ return protocol === "http:" || protocol === "https:";
193
+ } catch {
194
+ return false;
195
+ }
196
+ }
197
+ function commandFor(os, url) {
198
+ if (os === "darwin") return ["open", [url]];
199
+ if (os === "win32") return ["rundll32", ["url.dll,FileProtocolHandler", url]];
200
+ return ["xdg-open", [url]];
201
+ }
202
+ function openBrowser(url, deps = {}) {
203
+ if (!isHttpUrl(url)) return false;
204
+ const spawn = deps.spawn ?? ((file2, args2) => {
205
+ execFile(file2, args2, () => {
206
+ });
207
+ });
208
+ const [file, args] = commandFor(deps.platform ?? platform(), url);
209
+ spawn(file, args);
210
+ return true;
181
211
  }
182
- var loginCommand = new Command4("login").description("Authenticate with your DevelEmail instance via browser").requiredOption(
212
+
213
+ // src/commands/login.ts
214
+ var loginCommand = new Command4("login").description("Authenticate with your DevelEmail instance via browser").option(
183
215
  "--url <url>",
184
- "Base URL of your DevelEmail instance (e.g. https://develemail.com/api)"
216
+ "Base URL of your DevelEmail API",
217
+ "https://develemail.com/api"
185
218
  ).action(async (opts) => {
186
219
  const apiUrl = opts.url.replace(/\/$/, "");
187
220
  const baseUrl = apiUrl.replace(/\/api$/, "");
@@ -277,7 +310,22 @@ var listCommand = new Command5("list").description("List all projects").action(a
277
310
  );
278
311
  }
279
312
  });
280
- var createCommand = new Command5("create").description("Create a new project").requiredOption("--name <name>", "Project name (slug, e.g. my-app)").option(
313
+ var getCommand = new Command5("get").description("Get project details").argument("<id>", "Project ID").action(async (id) => {
314
+ const { url, token } = loadOwnerSession();
315
+ const project = await apiRequest(`${url}/projects/${id}`, token);
316
+ console.log(`ID: ${project.id}`);
317
+ console.log(`Name: ${project.name}`);
318
+ console.log(`Environment: ${project.environment}`);
319
+ console.log(`From Address: ${project.defaultFromAddress ?? "\u2014"}`);
320
+ console.log(
321
+ `Rate Limit: ${project.rateLimitPerDay ?? "Unlimited"} per day`
322
+ );
323
+ if (project.domainId) {
324
+ console.log(`Domain ID: ${project.domainId}`);
325
+ }
326
+ console.log(`Created At: ${new Date(project.createdAt).toISOString()}`);
327
+ });
328
+ var createCommand = new Command5("create").description("Create a new project").requiredOption("--name <name>", "Project name (slug, e.g. my-app)").option("--domain <domainId>", "Domain ID for this project").option(
281
329
  "--env <env>",
282
330
  "Environment: development | staging | production",
283
331
  "production"
@@ -288,6 +336,7 @@ var createCommand = new Command5("create").description("Create a new project").r
288
336
  method: "POST",
289
337
  body: JSON.stringify({
290
338
  name: opts.name,
339
+ domainId: opts.domain,
291
340
  environment: opts.env,
292
341
  defaultFromAddress: opts.from,
293
342
  rateLimitPerDay: opts.rateLimit ? Number(opts.rateLimit) : void 0,
@@ -297,10 +346,284 @@ var createCommand = new Command5("create").description("Create a new project").r
297
346
  console.log(`Created project: ${project.name} (${project.id})`);
298
347
  }
299
348
  );
300
- var projectCommand = new Command5("project").description("Manage projects").addCommand(listCommand).addCommand(createCommand);
349
+ var projectCommand = new Command5("project").description("Manage projects").addCommand(listCommand).addCommand(getCommand).addCommand(createCommand);
301
350
 
302
- // src/commands/key.ts
351
+ // src/commands/domains.ts
352
+ import { Command as Command7 } from "commander";
353
+
354
+ // src/owner-api.ts
355
+ var OwnerApiError = class extends Error {
356
+ constructor(message, status) {
357
+ super(message);
358
+ this.status = status;
359
+ }
360
+ status;
361
+ };
362
+ async function ownerRequest(path, init) {
363
+ const { url, token } = loadOwnerSession();
364
+ const res = await fetch(`${url}${path}`, {
365
+ ...init,
366
+ headers: {
367
+ Cookie: `session=${token}`,
368
+ "Content-Type": "application/json",
369
+ ...init?.headers ?? {}
370
+ }
371
+ });
372
+ if (res.status === 401) {
373
+ throw new OwnerApiError(
374
+ "Session expired or missing. Run `devel login` first.",
375
+ 401
376
+ );
377
+ }
378
+ if (!res.ok) {
379
+ const body = await res.json().catch(() => ({}));
380
+ throw new OwnerApiError(body.message ?? res.statusText, res.status);
381
+ }
382
+ return res.json();
383
+ }
384
+ function exitOnError(fn) {
385
+ return async (...args) => {
386
+ try {
387
+ await fn(...args);
388
+ } catch (err) {
389
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
390
+ process.exit(1);
391
+ }
392
+ };
393
+ }
394
+
395
+ // src/commands/dns-records.ts
396
+ var BIND_TTL = 3600;
397
+ function formatBind(records) {
398
+ return records.map((r) => {
399
+ const value = r.type === "TXT" ? `"${r.value.replace(/"/g, '\\"')}"` : r.value;
400
+ return `${r.host}. ${BIND_TTL} IN ${r.type} ${value}`;
401
+ }).join("\n");
402
+ }
403
+ function formatTable(records) {
404
+ return records.map(
405
+ (r) => `${r.priority.padEnd(11)} ${r.type.padEnd(5)} ${r.host}
406
+ ${r.value}
407
+ (${r.purpose})`
408
+ ).join("\n");
409
+ }
410
+ function printRecords(records, format, json) {
411
+ if (json) console.log(JSON.stringify(records, null, 2));
412
+ else if (format === "bind") console.log(formatBind(records));
413
+ else console.log(formatTable(records));
414
+ }
415
+
416
+ // src/commands/publish-dns.ts
303
417
  import { Command as Command6 } from "commander";
418
+ import { createInterface } from "readline/promises";
419
+ function pickZone(domainName, zones) {
420
+ const matches = zones.filter(
421
+ (z) => domainName === z.name || domainName.endsWith(`.${z.name}`)
422
+ );
423
+ if (matches.length === 0) {
424
+ throw new Error(`No Cloudflare zone matches ${domainName}.`);
425
+ }
426
+ const longest = Math.max(...matches.map((z) => z.name.length));
427
+ const best = matches.filter((z) => z.name.length === longest);
428
+ if (best.length > 1) {
429
+ throw new Error(
430
+ `Several zones match ${domainName}: ${best.map((z) => z.id).join(", ")}. Pass --zone.`
431
+ );
432
+ }
433
+ return best[0];
434
+ }
435
+ function printPlan(plan) {
436
+ for (const w of plan.warnings) console.log(`warning: ${w}`);
437
+ for (const c of plan.changes) {
438
+ const from = c.existingContent ? ` (was: ${c.existingContent})` : "";
439
+ console.log(
440
+ `${c.action.padEnd(6)} ${c.type} ${c.name} -> ${c.content}${from}`
441
+ );
442
+ for (const w of c.warnings) console.log(` warning: ${w}`);
443
+ }
444
+ }
445
+ async function confirm() {
446
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
447
+ try {
448
+ const answer = await rl.question("Apply these changes? [y/N] ");
449
+ return /^y(es)?$/i.test(answer.trim());
450
+ } finally {
451
+ rl.close();
452
+ }
453
+ }
454
+ var publishDnsCommand = new Command6("publish-dns").description("Publish a domain\u2019s DNS records to Cloudflare").argument("<id>", "Domain ID").option("--zone <zoneId>", "Cloudflare zone ID (default: auto-pick)").option("--yes", "Apply without prompting").option("--json", "Output JSON").action(
455
+ exitOnError(async (id, opts) => {
456
+ let zoneId = opts.zone;
457
+ if (!zoneId) {
458
+ const domain = await ownerRequest(`/domains/${id}`);
459
+ const zones = await ownerRequest("/cloudflare/zones");
460
+ zoneId = pickZone(domain.name, zones).id;
461
+ }
462
+ const plan = await ownerRequest(`/cloudflare/domains/${id}/plan`, {
463
+ method: "POST",
464
+ body: JSON.stringify({ zoneId })
465
+ });
466
+ if (!opts.json) printPlan(plan);
467
+ const apply = opts.yes || process.stdin.isTTY === true && await confirm();
468
+ if (!apply) {
469
+ if (opts.json) console.log(JSON.stringify({ plan, applied: false }));
470
+ else
471
+ console.log("Dry run: nothing applied. Re-run with --yes to apply.");
472
+ return;
473
+ }
474
+ const result = await ownerRequest("/cloudflare/apply", {
475
+ method: "POST",
476
+ body: JSON.stringify(plan)
477
+ });
478
+ if (opts.json)
479
+ console.log(JSON.stringify({ plan, applied: true, result }));
480
+ else console.log("Applied.");
481
+ })
482
+ );
483
+
484
+ // src/commands/domains.ts
485
+ var COL_ID3 = 40;
486
+ var COL_NAME2 = 30;
487
+ var COL_STATUS2 = 15;
488
+ function col3(s, w) {
489
+ return (s ?? "").slice(0, w).padEnd(w);
490
+ }
491
+ function printStatuses(d) {
492
+ console.log(`SPF Status: ${d.spfStatus ?? "\u2014"}`);
493
+ console.log(`DKIM Status: ${d.dkimStatus ?? "\u2014"}`);
494
+ console.log(`DMARC Status: ${d.dmarcStatus ?? "\u2014"}`);
495
+ }
496
+ var listCommand2 = new Command7("list").description("List all domains").action(
497
+ exitOnError(async () => {
498
+ const domains = await ownerRequest("/domains");
499
+ if (domains.length === 0) {
500
+ console.log("No domains found.");
501
+ return;
502
+ }
503
+ const header = `${col3("ID", COL_ID3)} ${col3("NAME", COL_NAME2)} ${col3("STATUS", COL_STATUS2)} CREATED`;
504
+ console.log(header);
505
+ console.log("-".repeat(header.length + 20));
506
+ for (const d of domains) {
507
+ const createdAt = new Date(d.createdAt).toLocaleDateString();
508
+ console.log(
509
+ `${col3(d.id, COL_ID3)} ${col3(d.name, COL_NAME2)} ${col3(d.verificationStatus, COL_STATUS2)} ${createdAt}`
510
+ );
511
+ }
512
+ })
513
+ );
514
+ var getCommand2 = new Command7("get").description("Get domain details").argument("<id>", "Domain ID").action(
515
+ exitOnError(async (id) => {
516
+ const domain = await ownerRequest(`/domains/${id}`);
517
+ console.log(`ID: ${domain.id}`);
518
+ console.log(`Name: ${domain.name}`);
519
+ console.log(`Verification Status: ${domain.verificationStatus}`);
520
+ printStatuses(domain);
521
+ console.log(`MX Status: ${domain.mxStatus ?? "\u2014"}`);
522
+ console.log(`Return Path Status: ${domain.returnPathStatus ?? "\u2014"}`);
523
+ console.log(`PTR Status: ${domain.ptrStatus ?? "\u2014"}`);
524
+ console.log(`DKIM Selector: ${domain.dkimSelector}`);
525
+ console.log(`DKIM Public Key:
526
+ ${domain.dkimPublicKey}`);
527
+ console.log(`Created At: ${new Date(domain.createdAt).toISOString()}`);
528
+ })
529
+ );
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(
531
+ exitOnError(
532
+ async (name, opts) => {
533
+ const domain = await ownerRequest("/domains", {
534
+ method: "POST",
535
+ body: JSON.stringify({ name, dkimSelector: opts.dkimSelector })
536
+ });
537
+ const records = await ownerRequest(
538
+ `/domains/${domain.id}/dns-records`
539
+ );
540
+ if (opts.json) {
541
+ console.log(JSON.stringify({ domain, records }, null, 2));
542
+ return;
543
+ }
544
+ console.log(`Created domain: ${domain.name} (${domain.id})`);
545
+ console.log("\nRequired DNS records:");
546
+ printRecords(records, "table", false);
547
+ }
548
+ )
549
+ );
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(
551
+ exitOnError(
552
+ async (id, opts) => {
553
+ const records = await ownerRequest(
554
+ `/domains/${id}/dns-records`
555
+ );
556
+ printRecords(records, opts.format, opts.json === true);
557
+ }
558
+ )
559
+ );
560
+ var POLL_INTERVAL_MS2 = 1e4;
561
+ 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(
563
+ exitOnError(
564
+ async (id, opts) => {
565
+ const deadline = Date.now() + Number(opts.timeout) * 1e3;
566
+ for (; ; ) {
567
+ const domain = await ownerRequest(
568
+ `/domains/${id}/verify`,
569
+ { method: "POST" }
570
+ );
571
+ console.log(`Domain verification: ${domain.name}`);
572
+ console.log(`Current status: ${domain.verificationStatus}`);
573
+ printStatuses(domain);
574
+ if (!opts.wait || domain.verificationStatus === "verified") return;
575
+ if (Date.now() + POLL_INTERVAL_MS2 > deadline) {
576
+ throw new Error(
577
+ `Timed out after ${opts.timeout}s; not yet verified.`
578
+ );
579
+ }
580
+ await sleep(POLL_INTERVAL_MS2);
581
+ }
582
+ }
583
+ )
584
+ );
585
+ var domainsCommand = new Command7("domains").description("Manage domains").addCommand(listCommand2).addCommand(getCommand2).addCommand(addCommand).addCommand(recordsCommand).addCommand(publishDnsCommand).addCommand(verifyCommand);
586
+
587
+ // 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(
590
+ "--token-env <VAR>",
591
+ "Environment variable holding the Cloudflare API token"
592
+ ).action(
593
+ exitOnError(async (opts) => {
594
+ const apiToken = process.env[opts.tokenEnv];
595
+ if (!apiToken) {
596
+ throw new Error(`Environment variable ${opts.tokenEnv} is not set.`);
597
+ }
598
+ await ownerRequest("/cloudflare/connect", {
599
+ method: "POST",
600
+ body: JSON.stringify({ apiToken })
601
+ });
602
+ console.log("Cloudflare connected.");
603
+ })
604
+ );
605
+ var statusCommand = new Command8("status").description("Show whether Cloudflare is connected").action(
606
+ exitOnError(async () => {
607
+ const { connected } = await ownerRequest(
608
+ "/cloudflare/status"
609
+ );
610
+ console.log(connected ? "Connected" : "Not connected");
611
+ })
612
+ );
613
+ var zonesCommand = new Command8("zones").description("List Cloudflare zones").option("--json", "Output JSON").action(
614
+ exitOnError(async (opts) => {
615
+ const zones = await ownerRequest("/cloudflare/zones");
616
+ if (opts.json) {
617
+ console.log(JSON.stringify(zones, null, 2));
618
+ return;
619
+ }
620
+ for (const z of zones) console.log(`${z.id} ${z.name} ${z.status}`);
621
+ })
622
+ );
623
+ var cloudflareCommand = new Command8("cloudflare").description("Manage the Cloudflare integration").addCommand(connectCommand).addCommand(statusCommand).addCommand(zonesCommand);
624
+
625
+ // src/commands/key.ts
626
+ import { Command as Command9 } from "commander";
304
627
  function ownerHeaders2(token) {
305
628
  return { Cookie: `session=${token}`, "Content-Type": "application/json" };
306
629
  }
@@ -316,7 +639,7 @@ async function apiRequest2(url, token, init) {
316
639
  }
317
640
  return res.json();
318
641
  }
319
- var listCommand2 = new Command6("list").description("List API keys for a project").requiredOption("--project <id>", "Project ID").action(async (opts) => {
642
+ var listCommand3 = new Command9("list").description("List API keys for a project").requiredOption("--project <id>", "Project ID").action(async (opts) => {
320
643
  const { url, token } = loadOwnerSession();
321
644
  const keys = await apiRequest2(
322
645
  `${url}/projects/${opts.project}/api-keys`,
@@ -331,7 +654,7 @@ var listCommand2 = new Command6("list").description("List API keys for a project
331
654
  console.log(`${status} ${k.keyPrefix}\u2026 ${k.name} (${k.id})`);
332
655
  }
333
656
  });
334
- var createCommand2 = new Command6("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(
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(
335
658
  async (opts) => {
336
659
  const { url, token } = loadOwnerSession();
337
660
  const result = await apiRequest2(
@@ -353,10 +676,10 @@ var createCommand2 = new Command6("create").description("Create a new API key fo
353
676
  console.log("Store the secret now \u2014 it will not be shown again.");
354
677
  }
355
678
  );
356
- var keyCommand = new Command6("key").description("Manage API keys").addCommand(listCommand2).addCommand(createCommand2);
679
+ var keyCommand = new Command9("key").description("Manage API keys").addCommand(listCommand3).addCommand(createCommand2);
357
680
 
358
681
  // src/commands/init.ts
359
- import { Command as Command7 } from "commander";
682
+ import { Command as Command10 } from "commander";
360
683
  import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
361
684
  import { resolve } from "path";
362
685
  function ownerHeaders3(token) {
@@ -386,7 +709,7 @@ function upsertEnvFile(filePath, vars) {
386
709
  const content = lines.join("\n").trimEnd() + (lines.some((l) => l.trim()) ? "\n" : "") + appended + "\n";
387
710
  writeFileSync2(filePath, content, "utf-8");
388
711
  }
389
- var initCommand = new Command7("init").description(
712
+ var initCommand = new Command10("init").description(
390
713
  "Provision a project + API key and write credentials to an env file"
391
714
  ).requiredOption(
392
715
  "--project <name>",
@@ -440,10 +763,12 @@ var initCommand = new Command7("init").description(
440
763
  );
441
764
 
442
765
  // src/index.ts
443
- var program = new Command8().name("devel").description("DevelEmail CLI \u2014 interact with your DevelEmail instance").version("0.1.0");
766
+ var program = new Command11().name("devel").description("DevelEmail CLI \u2014 interact with your DevelEmail instance").version("0.1.0");
444
767
  program.addCommand(loginCommand);
445
768
  program.addCommand(logoutCommand);
446
769
  program.addCommand(projectCommand);
770
+ program.addCommand(domainsCommand);
771
+ program.addCommand(cloudflareCommand);
447
772
  program.addCommand(keyCommand);
448
773
  program.addCommand(initCommand);
449
774
  program.addCommand(sendCommand);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@develemail/cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
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.1.1"
19
+ "@develemail/sdk": "0.4.0"
20
20
  },
21
21
  "devDependencies": {
22
22
  "tsup": "^8.3.5",