@develemail/cli 0.1.5 → 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.
- package/README.md +43 -0
- package/dist/index.js +703 -34
- 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/
|
|
4
|
-
import { Command as
|
|
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";
|
|
@@ -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(
|
|
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,20 +182,74 @@ import { Command as Command4 } from "commander";
|
|
|
173
182
|
import {
|
|
174
183
|
createServer
|
|
175
184
|
} from "http";
|
|
176
|
-
|
|
185
|
+
|
|
186
|
+
// src/open-browser.ts
|
|
187
|
+
import { execFile } from "child_process";
|
|
177
188
|
import { platform } from "os";
|
|
178
|
-
function
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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;
|
|
211
|
+
}
|
|
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
|
+
|
|
236
|
+
// src/commands/login.ts
|
|
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)"
|
|
186
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
|
+
}
|
|
187
247
|
const apiUrl = opts.url.replace(/\/$/, "");
|
|
188
|
-
const
|
|
248
|
+
const webAppUrl = await resolveWebAppUrl(apiUrl, fetch, {
|
|
249
|
+
appUrl: opts.appUrl
|
|
250
|
+
});
|
|
189
251
|
await new Promise((resolve2, reject) => {
|
|
252
|
+
let timeout;
|
|
190
253
|
const server = createServer(
|
|
191
254
|
(req, res) => {
|
|
192
255
|
const url = new URL(req.url ?? "/", `http://localhost`);
|
|
@@ -208,6 +271,7 @@ var loginCommand = new Command4("login").description("Authenticate with your Dev
|
|
|
208
271
|
);
|
|
209
272
|
console.log("Authenticated successfully.");
|
|
210
273
|
console.log("Session saved to ~/.config/develemail/auth.json");
|
|
274
|
+
clearTimeout(timeout);
|
|
211
275
|
server.close();
|
|
212
276
|
resolve2();
|
|
213
277
|
}
|
|
@@ -219,13 +283,16 @@ var loginCommand = new Command4("login").description("Authenticate with your Dev
|
|
|
219
283
|
return;
|
|
220
284
|
}
|
|
221
285
|
const port = addr.port;
|
|
222
|
-
const authUrl = `${
|
|
286
|
+
const authUrl = `${webAppUrl}/cli-auth?port=${port}`;
|
|
223
287
|
console.log("Opening browser for authentication...");
|
|
224
288
|
console.log(`If the browser doesn't open, visit: ${authUrl}`);
|
|
225
289
|
openBrowser(authUrl);
|
|
226
290
|
});
|
|
227
|
-
server.on("error",
|
|
228
|
-
|
|
291
|
+
server.on("error", (err) => {
|
|
292
|
+
clearTimeout(timeout);
|
|
293
|
+
reject(err);
|
|
294
|
+
});
|
|
295
|
+
timeout = setTimeout(
|
|
229
296
|
() => {
|
|
230
297
|
server.close();
|
|
231
298
|
reject(new Error("Authentication timed out after 5 minutes"));
|
|
@@ -262,6 +329,21 @@ var COL_ENV = 12;
|
|
|
262
329
|
function col2(s, w) {
|
|
263
330
|
return (s ?? "").slice(0, w).padEnd(w);
|
|
264
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
|
+
}
|
|
265
347
|
var listCommand = new Command5("list").description("List all projects").action(async () => {
|
|
266
348
|
const { url, token } = loadOwnerSession();
|
|
267
349
|
const projects = await apiRequest(`${url}/projects`, token);
|
|
@@ -278,7 +360,25 @@ var listCommand = new Command5("list").description("List all projects").action(a
|
|
|
278
360
|
);
|
|
279
361
|
}
|
|
280
362
|
});
|
|
281
|
-
var
|
|
363
|
+
var getCommand = new Command5("get").description("Get project details").argument("<id>", "Project ID").action(async (id) => {
|
|
364
|
+
const { url, token } = loadOwnerSession();
|
|
365
|
+
const project = await apiRequest(`${url}/projects/${id}`, token);
|
|
366
|
+
console.log(`ID: ${project.id}`);
|
|
367
|
+
console.log(`Name: ${project.name}`);
|
|
368
|
+
console.log(`Environment: ${project.environment}`);
|
|
369
|
+
console.log(`From Address: ${project.defaultFromAddress ?? "\u2014"}`);
|
|
370
|
+
console.log(
|
|
371
|
+
`Rate Limit: ${project.rateLimitPerDay ?? "Unlimited"} per day`
|
|
372
|
+
);
|
|
373
|
+
console.log(
|
|
374
|
+
`Recipient Cooldown: ${formatCooldown(project.recipientCooldownMinutes)}`
|
|
375
|
+
);
|
|
376
|
+
if (project.domainId) {
|
|
377
|
+
console.log(`Domain ID: ${project.domainId}`);
|
|
378
|
+
}
|
|
379
|
+
console.log(`Created At: ${new Date(project.createdAt).toISOString()}`);
|
|
380
|
+
});
|
|
381
|
+
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
382
|
"--env <env>",
|
|
283
383
|
"Environment: development | staging | production",
|
|
284
384
|
"production"
|
|
@@ -289,6 +389,7 @@ var createCommand = new Command5("create").description("Create a new project").r
|
|
|
289
389
|
method: "POST",
|
|
290
390
|
body: JSON.stringify({
|
|
291
391
|
name: opts.name,
|
|
392
|
+
domainId: opts.domain,
|
|
292
393
|
environment: opts.env,
|
|
293
394
|
defaultFromAddress: opts.from,
|
|
294
395
|
rateLimitPerDay: opts.rateLimit ? Number(opts.rateLimit) : void 0,
|
|
@@ -298,10 +399,563 @@ var createCommand = new Command5("create").description("Create a new project").r
|
|
|
298
399
|
console.log(`Created project: ${project.name} (${project.id})`);
|
|
299
400
|
}
|
|
300
401
|
);
|
|
301
|
-
var
|
|
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);
|
|
302
426
|
|
|
303
|
-
// src/commands/
|
|
427
|
+
// src/commands/domains.ts
|
|
428
|
+
import { Command as Command8 } from "commander";
|
|
429
|
+
|
|
430
|
+
// src/owner-api.ts
|
|
431
|
+
var OwnerApiError = class extends Error {
|
|
432
|
+
constructor(message, status) {
|
|
433
|
+
super(message);
|
|
434
|
+
this.status = status;
|
|
435
|
+
}
|
|
436
|
+
status;
|
|
437
|
+
};
|
|
438
|
+
async function ownerRequest(path, init) {
|
|
439
|
+
const { url, token } = loadOwnerSession();
|
|
440
|
+
const res = await fetch(`${url}${path}`, {
|
|
441
|
+
...init,
|
|
442
|
+
headers: {
|
|
443
|
+
Cookie: `session=${token}`,
|
|
444
|
+
...init?.body != null ? { "Content-Type": "application/json" } : {},
|
|
445
|
+
...init?.headers ?? {}
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
if (res.status === 401) {
|
|
449
|
+
throw new OwnerApiError(
|
|
450
|
+
"Session expired or missing. Run `devel login` first.",
|
|
451
|
+
401
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
if (!res.ok) {
|
|
455
|
+
const body = await res.json().catch(() => ({}));
|
|
456
|
+
throw new OwnerApiError(body.message ?? res.statusText, res.status);
|
|
457
|
+
}
|
|
458
|
+
if (res.status === 204) return void 0;
|
|
459
|
+
return res.json();
|
|
460
|
+
}
|
|
461
|
+
function exitOnError(fn) {
|
|
462
|
+
return async (...args) => {
|
|
463
|
+
try {
|
|
464
|
+
await fn(...args);
|
|
465
|
+
} catch (err) {
|
|
466
|
+
console.error(`Error: ${err instanceof Error ? err.message : err}`);
|
|
467
|
+
process.exit(1);
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// src/commands/dns-records.ts
|
|
473
|
+
var BIND_TTL = 3600;
|
|
474
|
+
function formatBind(records) {
|
|
475
|
+
return records.map((r) => {
|
|
476
|
+
const value = r.type === "TXT" ? `"${r.value.replace(/"/g, '\\"')}"` : r.value;
|
|
477
|
+
return `${r.host}. ${BIND_TTL} IN ${r.type} ${value}`;
|
|
478
|
+
}).join("\n");
|
|
479
|
+
}
|
|
480
|
+
function formatTable(records) {
|
|
481
|
+
return records.map(
|
|
482
|
+
(r) => `${r.priority.padEnd(11)} ${r.type.padEnd(5)} ${r.host}
|
|
483
|
+
${r.value}
|
|
484
|
+
(${r.purpose})`
|
|
485
|
+
).join("\n");
|
|
486
|
+
}
|
|
487
|
+
function printRecords(records, format, json) {
|
|
488
|
+
if (json) console.log(JSON.stringify(records, null, 2));
|
|
489
|
+
else if (format === "bind") console.log(formatBind(records));
|
|
490
|
+
else console.log(formatTable(records));
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// src/commands/publish-dns.ts
|
|
304
494
|
import { Command as Command6 } from "commander";
|
|
495
|
+
import { createInterface } from "readline/promises";
|
|
496
|
+
function pickZone(domainName, zones) {
|
|
497
|
+
const matches = zones.filter(
|
|
498
|
+
(z) => domainName === z.name || domainName.endsWith(`.${z.name}`)
|
|
499
|
+
);
|
|
500
|
+
if (matches.length === 0) {
|
|
501
|
+
throw new Error(`No Cloudflare zone matches ${domainName}.`);
|
|
502
|
+
}
|
|
503
|
+
const longest = Math.max(...matches.map((z) => z.name.length));
|
|
504
|
+
const best = matches.filter((z) => z.name.length === longest);
|
|
505
|
+
if (best.length > 1) {
|
|
506
|
+
throw new Error(
|
|
507
|
+
`Several zones match ${domainName}: ${best.map((z) => z.id).join(", ")}. Pass --zone.`
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
return best[0];
|
|
511
|
+
}
|
|
512
|
+
function printPlan(plan) {
|
|
513
|
+
for (const w of plan.warnings) console.log(`warning: ${w}`);
|
|
514
|
+
for (const c of plan.changes) {
|
|
515
|
+
const from = c.existingContent ? ` (was: ${c.existingContent})` : "";
|
|
516
|
+
console.log(
|
|
517
|
+
`${c.action.padEnd(6)} ${c.type} ${c.name} -> ${c.content}${from}`
|
|
518
|
+
);
|
|
519
|
+
for (const w of c.warnings) console.log(` warning: ${w}`);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
async function confirm() {
|
|
523
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
524
|
+
try {
|
|
525
|
+
const answer = await rl.question("Apply these changes? [y/N] ");
|
|
526
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
527
|
+
} finally {
|
|
528
|
+
rl.close();
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
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(
|
|
532
|
+
exitOnError(async (id, opts) => {
|
|
533
|
+
let zoneId = opts.zone;
|
|
534
|
+
if (!zoneId) {
|
|
535
|
+
const domain = await ownerRequest(`/domains/${id}`);
|
|
536
|
+
const zones = await ownerRequest("/cloudflare/zones");
|
|
537
|
+
zoneId = pickZone(domain.name, zones).id;
|
|
538
|
+
}
|
|
539
|
+
const plan = await ownerRequest(`/cloudflare/domains/${id}/plan`, {
|
|
540
|
+
method: "POST",
|
|
541
|
+
body: JSON.stringify({ zoneId })
|
|
542
|
+
});
|
|
543
|
+
if (!opts.json) printPlan(plan);
|
|
544
|
+
const apply = opts.yes || process.stdin.isTTY === true && await confirm();
|
|
545
|
+
if (!apply) {
|
|
546
|
+
if (opts.json) console.log(JSON.stringify({ plan, applied: false }));
|
|
547
|
+
else
|
|
548
|
+
console.log("Dry run: nothing applied. Re-run with --yes to apply.");
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
const result = await ownerRequest("/cloudflare/apply", {
|
|
552
|
+
method: "POST",
|
|
553
|
+
body: JSON.stringify(plan)
|
|
554
|
+
});
|
|
555
|
+
if (opts.json)
|
|
556
|
+
console.log(JSON.stringify({ plan, applied: true, result }));
|
|
557
|
+
else console.log("Applied.");
|
|
558
|
+
})
|
|
559
|
+
);
|
|
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
|
+
|
|
618
|
+
// src/commands/domains.ts
|
|
619
|
+
var COL_ID3 = 40;
|
|
620
|
+
var COL_NAME2 = 30;
|
|
621
|
+
var COL_STATUS2 = 15;
|
|
622
|
+
function col3(s, w) {
|
|
623
|
+
return (s ?? "").slice(0, w).padEnd(w);
|
|
624
|
+
}
|
|
625
|
+
function printStatuses(d) {
|
|
626
|
+
console.log(`SPF Status: ${d.spfStatus ?? "\u2014"}`);
|
|
627
|
+
console.log(`DKIM Status: ${d.dkimStatus ?? "\u2014"}`);
|
|
628
|
+
console.log(`DMARC Status: ${d.dmarcStatus ?? "\u2014"}`);
|
|
629
|
+
}
|
|
630
|
+
var listCommand2 = new Command8("list").description("List all domains").action(
|
|
631
|
+
exitOnError(async () => {
|
|
632
|
+
const domains = await ownerRequest("/domains");
|
|
633
|
+
if (domains.length === 0) {
|
|
634
|
+
console.log("No domains found.");
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
const header = `${col3("ID", COL_ID3)} ${col3("NAME", COL_NAME2)} ${col3("STATUS", COL_STATUS2)} CREATED`;
|
|
638
|
+
console.log(header);
|
|
639
|
+
console.log("-".repeat(header.length + 20));
|
|
640
|
+
for (const d of domains) {
|
|
641
|
+
const createdAt = new Date(d.createdAt).toLocaleDateString();
|
|
642
|
+
console.log(
|
|
643
|
+
`${col3(d.id, COL_ID3)} ${col3(d.name, COL_NAME2)} ${col3(d.verificationStatus, COL_STATUS2)} ${createdAt}`
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
})
|
|
647
|
+
);
|
|
648
|
+
var getCommand2 = new Command8("get").description("Get domain details").argument("<id>", "Domain ID").action(
|
|
649
|
+
exitOnError(async (id) => {
|
|
650
|
+
const domain = await ownerRequest(`/domains/${id}`);
|
|
651
|
+
console.log(`ID: ${domain.id}`);
|
|
652
|
+
console.log(`Name: ${domain.name}`);
|
|
653
|
+
console.log(`Verification Status: ${domain.verificationStatus}`);
|
|
654
|
+
printStatuses(domain);
|
|
655
|
+
console.log(`MX Status: ${domain.mxStatus ?? "\u2014"}`);
|
|
656
|
+
console.log(`Return Path Status: ${domain.returnPathStatus ?? "\u2014"}`);
|
|
657
|
+
console.log(`PTR Status: ${domain.ptrStatus ?? "\u2014"}`);
|
|
658
|
+
console.log(`DKIM Selector: ${domain.dkimSelector}`);
|
|
659
|
+
console.log(`DKIM Public Key:
|
|
660
|
+
${domain.dkimPublicKey}`);
|
|
661
|
+
console.log(`Created At: ${new Date(domain.createdAt).toISOString()}`);
|
|
662
|
+
})
|
|
663
|
+
);
|
|
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(
|
|
665
|
+
exitOnError(
|
|
666
|
+
async (name, opts) => {
|
|
667
|
+
const domain = await ownerRequest("/domains", {
|
|
668
|
+
method: "POST",
|
|
669
|
+
body: JSON.stringify({ name, dkimSelector: opts.dkimSelector })
|
|
670
|
+
});
|
|
671
|
+
const records = await ownerRequest(
|
|
672
|
+
`/domains/${domain.id}/dns-records`
|
|
673
|
+
);
|
|
674
|
+
if (opts.json) {
|
|
675
|
+
console.log(JSON.stringify({ domain, records }, null, 2));
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
console.log(`Created domain: ${domain.name} (${domain.id})`);
|
|
679
|
+
console.log("\nRequired DNS records:");
|
|
680
|
+
printRecords(records, "table", false);
|
|
681
|
+
}
|
|
682
|
+
)
|
|
683
|
+
);
|
|
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(
|
|
685
|
+
exitOnError(
|
|
686
|
+
async (id, opts) => {
|
|
687
|
+
const records = await ownerRequest(
|
|
688
|
+
`/domains/${id}/dns-records`
|
|
689
|
+
);
|
|
690
|
+
printRecords(records, opts.format, opts.json === true);
|
|
691
|
+
}
|
|
692
|
+
)
|
|
693
|
+
);
|
|
694
|
+
var POLL_INTERVAL_MS2 = 1e4;
|
|
695
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
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(
|
|
697
|
+
exitOnError(
|
|
698
|
+
async (id, opts) => {
|
|
699
|
+
const deadline = Date.now() + Number(opts.timeout) * 1e3;
|
|
700
|
+
for (; ; ) {
|
|
701
|
+
const domain = await ownerRequest(
|
|
702
|
+
`/domains/${id}/verify`,
|
|
703
|
+
{ method: "POST" }
|
|
704
|
+
);
|
|
705
|
+
console.log(`Domain verification: ${domain.name}`);
|
|
706
|
+
console.log(`Current status: ${domain.verificationStatus}`);
|
|
707
|
+
printStatuses(domain);
|
|
708
|
+
if (!opts.wait || domain.verificationStatus === "verified") return;
|
|
709
|
+
if (Date.now() + POLL_INTERVAL_MS2 > deadline) {
|
|
710
|
+
throw new Error(
|
|
711
|
+
`Timed out after ${opts.timeout}s; not yet verified.`
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
await sleep(POLL_INTERVAL_MS2);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
)
|
|
718
|
+
);
|
|
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);
|
|
918
|
+
|
|
919
|
+
// src/commands/cloudflare.ts
|
|
920
|
+
import { Command as Command10 } from "commander";
|
|
921
|
+
var connectCommand = new Command10("connect").description("Connect a Cloudflare API token to your organization").requiredOption(
|
|
922
|
+
"--token-env <VAR>",
|
|
923
|
+
"Environment variable holding the Cloudflare API token"
|
|
924
|
+
).action(
|
|
925
|
+
exitOnError(async (opts) => {
|
|
926
|
+
const apiToken = process.env[opts.tokenEnv];
|
|
927
|
+
if (!apiToken) {
|
|
928
|
+
throw new Error(`Environment variable ${opts.tokenEnv} is not set.`);
|
|
929
|
+
}
|
|
930
|
+
await ownerRequest("/cloudflare/connect", {
|
|
931
|
+
method: "POST",
|
|
932
|
+
body: JSON.stringify({ apiToken })
|
|
933
|
+
});
|
|
934
|
+
console.log("Cloudflare connected.");
|
|
935
|
+
})
|
|
936
|
+
);
|
|
937
|
+
var statusCommand = new Command10("status").description("Show whether Cloudflare is connected").action(
|
|
938
|
+
exitOnError(async () => {
|
|
939
|
+
const { connected } = await ownerRequest(
|
|
940
|
+
"/cloudflare/status"
|
|
941
|
+
);
|
|
942
|
+
console.log(connected ? "Connected" : "Not connected");
|
|
943
|
+
})
|
|
944
|
+
);
|
|
945
|
+
var zonesCommand = new Command10("zones").description("List Cloudflare zones").option("--json", "Output JSON").action(
|
|
946
|
+
exitOnError(async (opts) => {
|
|
947
|
+
const zones = await ownerRequest("/cloudflare/zones");
|
|
948
|
+
if (opts.json) {
|
|
949
|
+
console.log(JSON.stringify(zones, null, 2));
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
for (const z of zones) console.log(`${z.id} ${z.name} ${z.status}`);
|
|
953
|
+
})
|
|
954
|
+
);
|
|
955
|
+
var cloudflareCommand = new Command10("cloudflare").description("Manage the Cloudflare integration").addCommand(connectCommand).addCommand(statusCommand).addCommand(zonesCommand);
|
|
956
|
+
|
|
957
|
+
// src/commands/key.ts
|
|
958
|
+
import { Command as Command11 } from "commander";
|
|
305
959
|
function ownerHeaders2(token) {
|
|
306
960
|
return { Cookie: `session=${token}`, "Content-Type": "application/json" };
|
|
307
961
|
}
|
|
@@ -317,7 +971,7 @@ async function apiRequest2(url, token, init) {
|
|
|
317
971
|
}
|
|
318
972
|
return res.json();
|
|
319
973
|
}
|
|
320
|
-
var
|
|
974
|
+
var listCommand4 = new Command11("list").description("List API keys for a project").requiredOption("--project <id>", "Project ID").action(async (opts) => {
|
|
321
975
|
const { url, token } = loadOwnerSession();
|
|
322
976
|
const keys = await apiRequest2(
|
|
323
977
|
`${url}/projects/${opts.project}/api-keys`,
|
|
@@ -332,7 +986,7 @@ var listCommand2 = new Command6("list").description("List API keys for a project
|
|
|
332
986
|
console.log(`${status} ${k.keyPrefix}\u2026 ${k.name} (${k.id})`);
|
|
333
987
|
}
|
|
334
988
|
});
|
|
335
|
-
var createCommand2 = new
|
|
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(
|
|
336
990
|
async (opts) => {
|
|
337
991
|
const { url, token } = loadOwnerSession();
|
|
338
992
|
const result = await apiRequest2(
|
|
@@ -354,10 +1008,10 @@ var createCommand2 = new Command6("create").description("Create a new API key fo
|
|
|
354
1008
|
console.log("Store the secret now \u2014 it will not be shown again.");
|
|
355
1009
|
}
|
|
356
1010
|
);
|
|
357
|
-
var keyCommand = new
|
|
1011
|
+
var keyCommand = new Command11("key").description("Manage API keys").addCommand(listCommand4).addCommand(createCommand2);
|
|
358
1012
|
|
|
359
1013
|
// src/commands/init.ts
|
|
360
|
-
import { Command as
|
|
1014
|
+
import { Command as Command12 } from "commander";
|
|
361
1015
|
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
362
1016
|
import { resolve } from "path";
|
|
363
1017
|
function ownerHeaders3(token) {
|
|
@@ -387,7 +1041,7 @@ function upsertEnvFile(filePath, vars) {
|
|
|
387
1041
|
const content = lines.join("\n").trimEnd() + (lines.some((l) => l.trim()) ? "\n" : "") + appended + "\n";
|
|
388
1042
|
writeFileSync2(filePath, content, "utf-8");
|
|
389
1043
|
}
|
|
390
|
-
var initCommand = new
|
|
1044
|
+
var initCommand = new Command12("init").description(
|
|
391
1045
|
"Provision a project + API key and write credentials to an env file"
|
|
392
1046
|
).requiredOption(
|
|
393
1047
|
"--project <name>",
|
|
@@ -440,14 +1094,29 @@ var initCommand = new Command7("init").description(
|
|
|
440
1094
|
}
|
|
441
1095
|
);
|
|
442
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
|
+
|
|
443
1121
|
// src/index.ts
|
|
444
|
-
|
|
445
|
-
program.addCommand(loginCommand);
|
|
446
|
-
program.addCommand(logoutCommand);
|
|
447
|
-
program.addCommand(projectCommand);
|
|
448
|
-
program.addCommand(keyCommand);
|
|
449
|
-
program.addCommand(initCommand);
|
|
450
|
-
program.addCommand(sendCommand);
|
|
451
|
-
program.addCommand(logsCommand);
|
|
452
|
-
program.addCommand(tailCommand);
|
|
453
|
-
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.
|
|
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.
|
|
19
|
+
"@develemail/sdk": "0.4.1"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"tsup": "^8.3.5",
|