@develemail/cli 0.1.5 → 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 +339 -15
  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,12 +182,35 @@ 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
  }
212
+
213
+ // src/commands/login.ts
182
214
  var loginCommand = new Command4("login").description("Authenticate with your DevelEmail instance via browser").option(
183
215
  "--url <url>",
184
216
  "Base URL of your DevelEmail API",
@@ -278,7 +310,22 @@ var listCommand = new Command5("list").description("List all projects").action(a
278
310
  );
279
311
  }
280
312
  });
281
- 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(
282
329
  "--env <env>",
283
330
  "Environment: development | staging | production",
284
331
  "production"
@@ -289,6 +336,7 @@ var createCommand = new Command5("create").description("Create a new project").r
289
336
  method: "POST",
290
337
  body: JSON.stringify({
291
338
  name: opts.name,
339
+ domainId: opts.domain,
292
340
  environment: opts.env,
293
341
  defaultFromAddress: opts.from,
294
342
  rateLimitPerDay: opts.rateLimit ? Number(opts.rateLimit) : void 0,
@@ -298,10 +346,284 @@ var createCommand = new Command5("create").description("Create a new project").r
298
346
  console.log(`Created project: ${project.name} (${project.id})`);
299
347
  }
300
348
  );
301
- 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);
302
350
 
303
- // 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
304
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";
305
627
  function ownerHeaders2(token) {
306
628
  return { Cookie: `session=${token}`, "Content-Type": "application/json" };
307
629
  }
@@ -317,7 +639,7 @@ async function apiRequest2(url, token, init) {
317
639
  }
318
640
  return res.json();
319
641
  }
320
- 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) => {
321
643
  const { url, token } = loadOwnerSession();
322
644
  const keys = await apiRequest2(
323
645
  `${url}/projects/${opts.project}/api-keys`,
@@ -332,7 +654,7 @@ var listCommand2 = new Command6("list").description("List API keys for a project
332
654
  console.log(`${status} ${k.keyPrefix}\u2026 ${k.name} (${k.id})`);
333
655
  }
334
656
  });
335
- 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(
336
658
  async (opts) => {
337
659
  const { url, token } = loadOwnerSession();
338
660
  const result = await apiRequest2(
@@ -354,10 +676,10 @@ var createCommand2 = new Command6("create").description("Create a new API key fo
354
676
  console.log("Store the secret now \u2014 it will not be shown again.");
355
677
  }
356
678
  );
357
- 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);
358
680
 
359
681
  // src/commands/init.ts
360
- import { Command as Command7 } from "commander";
682
+ import { Command as Command10 } from "commander";
361
683
  import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
362
684
  import { resolve } from "path";
363
685
  function ownerHeaders3(token) {
@@ -387,7 +709,7 @@ function upsertEnvFile(filePath, vars) {
387
709
  const content = lines.join("\n").trimEnd() + (lines.some((l) => l.trim()) ? "\n" : "") + appended + "\n";
388
710
  writeFileSync2(filePath, content, "utf-8");
389
711
  }
390
- var initCommand = new Command7("init").description(
712
+ var initCommand = new Command10("init").description(
391
713
  "Provision a project + API key and write credentials to an env file"
392
714
  ).requiredOption(
393
715
  "--project <name>",
@@ -441,10 +763,12 @@ var initCommand = new Command7("init").description(
441
763
  );
442
764
 
443
765
  // src/index.ts
444
- 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");
445
767
  program.addCommand(loginCommand);
446
768
  program.addCommand(logoutCommand);
447
769
  program.addCommand(projectCommand);
770
+ program.addCommand(domainsCommand);
771
+ program.addCommand(cloudflareCommand);
448
772
  program.addCommand(keyCommand);
449
773
  program.addCommand(initCommand);
450
774
  program.addCommand(sendCommand);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@develemail/cli",
3
- "version": "0.1.5",
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",