@develemail/cli 0.1.1

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 +433 -0
  2. package/package.json +28 -0
package/dist/index.js ADDED
@@ -0,0 +1,433 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command as Command8 } from "commander";
5
+
6
+ // src/commands/send.ts
7
+ import { Command } from "commander";
8
+ import { DevelEmail, DevelEmailError } from "@develemail/sdk";
9
+
10
+ // src/config.ts
11
+ import { config } from "dotenv";
12
+ import { homedir } from "os";
13
+ import { join } from "path";
14
+ import { readFileSync, writeFileSync, unlinkSync, mkdirSync } from "fs";
15
+ config();
16
+ function loadConfig() {
17
+ const apiUrl = process.env.DEVELEMAIL_BASE_URL ?? process.env.DEVELEMAIL_API_URL;
18
+ const apiKey = process.env.DEVELEMAIL_API_KEY;
19
+ if (!apiUrl) {
20
+ console.error("Error: DEVELEMAIL_BASE_URL is not set.");
21
+ console.error(
22
+ "Set it in your environment or a .env file in the current directory."
23
+ );
24
+ process.exit(1);
25
+ }
26
+ if (!apiKey) {
27
+ console.error("Error: DEVELEMAIL_API_KEY is not set.");
28
+ console.error(
29
+ "Set it in your environment or a .env file in the current directory."
30
+ );
31
+ process.exit(1);
32
+ }
33
+ return { apiUrl, apiKey };
34
+ }
35
+ var AUTH_DIR = join(homedir(), ".config", "develemail");
36
+ var AUTH_FILE = join(AUTH_DIR, "auth.json");
37
+ function loadOwnerSession() {
38
+ try {
39
+ const raw = readFileSync(AUTH_FILE, "utf-8");
40
+ return JSON.parse(raw);
41
+ } catch {
42
+ console.error("Not logged in. Run `devel login --url <url>` first.");
43
+ process.exit(1);
44
+ }
45
+ }
46
+ function saveOwnerSession(session) {
47
+ mkdirSync(AUTH_DIR, { recursive: true });
48
+ writeFileSync(AUTH_FILE, JSON.stringify(session, null, 2), { mode: 384 });
49
+ }
50
+ function clearOwnerSession() {
51
+ try {
52
+ unlinkSync(AUTH_FILE);
53
+ } catch {
54
+ }
55
+ }
56
+
57
+ // src/commands/send.ts
58
+ var sendCommand = new Command("send").description("Send a test email").requiredOption("--to <email>", "Recipient email address").requiredOption("--subject <subject>", "Email subject").option(
59
+ "--from <from>",
60
+ "Sender address",
61
+ process.env["DEVELEMAIL_FROM"] ?? "test@develemail.local"
62
+ ).option("--html <html>", "HTML body").action(
63
+ async (opts) => {
64
+ const cfg = loadConfig();
65
+ const client = new DevelEmail({
66
+ apiKey: cfg.apiKey,
67
+ baseUrl: cfg.apiUrl
68
+ });
69
+ try {
70
+ const result = await client.emails.send({
71
+ to: opts.to,
72
+ subject: opts.subject,
73
+ from: opts.from,
74
+ html: opts.html ?? `<p>${opts.subject}</p>`
75
+ });
76
+ console.log(result.id);
77
+ } catch (err) {
78
+ const msg = err instanceof DevelEmailError ? err.message : err instanceof Error ? err.message : String(err);
79
+ console.error(`Error: ${msg}`);
80
+ process.exit(1);
81
+ }
82
+ }
83
+ );
84
+
85
+ // src/commands/logs.ts
86
+ import { Command as Command2 } from "commander";
87
+ var COL_ID = 36;
88
+ var COL_FROM = 28;
89
+ var COL_SUBJECT = 38;
90
+ var COL_STATUS = 10;
91
+ function col(s, w) {
92
+ return s.slice(0, w).padEnd(w);
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) => {
95
+ const cfg = loadConfig();
96
+ const params = new URLSearchParams({ limit: opts.limit });
97
+ if (opts.status) params.set("status", opts.status);
98
+ let res;
99
+ try {
100
+ res = await fetch(`${cfg.apiUrl}/emails?${params.toString()}`, {
101
+ headers: { Authorization: `Bearer ${cfg.apiKey}` }
102
+ });
103
+ } catch (err) {
104
+ console.error(
105
+ `Error: ${err instanceof Error ? err.message : String(err)}`
106
+ );
107
+ process.exit(1);
108
+ }
109
+ if (!res.ok) {
110
+ const body = await res.json().catch(() => ({}));
111
+ console.error(`Error: ${body.message ?? res.statusText}`);
112
+ process.exit(1);
113
+ }
114
+ const result = await res.json();
115
+ const rows = result.data ?? [];
116
+ if (rows.length === 0) {
117
+ console.log("No emails found.");
118
+ return;
119
+ }
120
+ const header = `${col("ID", COL_ID)} ${col("FROM", COL_FROM)} ${col("SUBJECT", COL_SUBJECT)} ${col("STATUS", COL_STATUS)} CREATED`;
121
+ console.log(header);
122
+ console.log("-".repeat(header.length));
123
+ for (const row of rows) {
124
+ console.log(
125
+ `${col(row.id, COL_ID)} ${col(row.fromAddress, COL_FROM)} ${col(row.subject ?? "", COL_SUBJECT)} ${col(row.status, COL_STATUS)} ${new Date(row.createdAt).toLocaleString()}`
126
+ );
127
+ }
128
+ console.log(`
129
+ ${rows.length} of ${result.total} emails`);
130
+ });
131
+
132
+ // src/commands/tail.ts
133
+ import { Command as Command3 } from "commander";
134
+ var POLL_INTERVAL_MS = 5e3;
135
+ var tailCommand = new Command3("tail").description("Poll and print new email events (Ctrl-C to stop)").option(
136
+ "--since <timestamp>",
137
+ "Skip events before this ISO timestamp (default: now)"
138
+ ).action(async (opts) => {
139
+ const cfg = loadConfig();
140
+ const seen = /* @__PURE__ */ new Set();
141
+ const startedAt = opts.since ? new Date(opts.since) : /* @__PURE__ */ new Date();
142
+ console.log("Tailing email events\u2026 (Ctrl-C to stop)");
143
+ const poll = async () => {
144
+ try {
145
+ const res = await fetch(
146
+ `${cfg.apiUrl}/emails/recent-activity?limit=20`,
147
+ { headers: { Authorization: `Bearer ${cfg.apiKey}` } }
148
+ );
149
+ if (!res.ok) {
150
+ console.error(`Poll error: ${res.statusText}`);
151
+ return;
152
+ }
153
+ const items = await res.json();
154
+ for (const item of items) {
155
+ if (!seen.has(item.id)) {
156
+ seen.add(item.id);
157
+ if (new Date(item.createdAt) < startedAt) continue;
158
+ const ts = new Date(item.createdAt).toLocaleTimeString();
159
+ console.log(
160
+ `[${ts}] ${item.type.padEnd(12)} ${item.subject} (${item.fromAddress})`
161
+ );
162
+ }
163
+ }
164
+ } catch {
165
+ }
166
+ };
167
+ await poll();
168
+ setInterval(() => void poll(), POLL_INTERVAL_MS);
169
+ });
170
+
171
+ // src/commands/login.ts
172
+ import { Command as Command4 } from "commander";
173
+ import { createInterface } from "readline/promises";
174
+ import { stdin, stdout } from "process";
175
+ async function prompt(question) {
176
+ const rl = createInterface({ input: stdin, output: stdout });
177
+ const answer = await rl.question(question);
178
+ rl.close();
179
+ return answer.trim();
180
+ }
181
+ var loginCommand = new Command4("login").description("Authenticate with your DevelEmail instance as owner").requiredOption(
182
+ "--url <url>",
183
+ "Base URL of your DevelEmail API (e.g. https://mail.example.com)"
184
+ ).option("--email <email>", "Owner email").option("--password <password>", "Owner password").action(async (opts) => {
185
+ const url = opts.url.replace(/\/$/, "");
186
+ const email = opts.email ?? await prompt("Email: ");
187
+ const password = opts.password ?? await prompt("Password: ");
188
+ let res;
189
+ try {
190
+ res = await fetch(`${url}/auth/login`, {
191
+ method: "POST",
192
+ headers: { "Content-Type": "application/json" },
193
+ body: JSON.stringify({ email, password })
194
+ });
195
+ } catch (err) {
196
+ console.error(
197
+ `Error: ${err instanceof Error ? err.message : String(err)}`
198
+ );
199
+ process.exit(1);
200
+ }
201
+ if (!res.ok) {
202
+ const body = await res.json().catch(() => ({}));
203
+ console.error(`Error: ${body.message ?? res.statusText}`);
204
+ process.exit(1);
205
+ }
206
+ const setCookie = res.headers.get("set-cookie");
207
+ const token = setCookie?.match(/session=([^;]+)/)?.[1];
208
+ if (!token) {
209
+ console.error("Error: No session cookie returned from server.");
210
+ process.exit(1);
211
+ }
212
+ saveOwnerSession({ url, token });
213
+ const data = await res.json();
214
+ console.log(`Logged in as ${data.owner?.email ?? email}`);
215
+ console.log(`Session saved to ~/.config/develemail/auth.json`);
216
+ });
217
+ var logoutCommand = new Command4("logout").description("Clear the stored owner session").action(() => {
218
+ clearOwnerSession();
219
+ console.log("Logged out.");
220
+ });
221
+
222
+ // src/commands/project.ts
223
+ import { Command as Command5 } from "commander";
224
+ function ownerHeaders(token) {
225
+ return { Cookie: `session=${token}`, "Content-Type": "application/json" };
226
+ }
227
+ async function apiRequest(url, token, init) {
228
+ const res = await fetch(url, {
229
+ ...init,
230
+ headers: { ...ownerHeaders(token), ...init?.headers ?? {} }
231
+ });
232
+ if (!res.ok) {
233
+ const body = await res.json().catch(() => ({}));
234
+ console.error(`Error: ${body.message ?? res.statusText}`);
235
+ process.exit(1);
236
+ }
237
+ return res.json();
238
+ }
239
+ var COL_ID2 = 36;
240
+ var COL_NAME = 24;
241
+ var COL_ENV = 12;
242
+ function col2(s, w) {
243
+ return (s ?? "").slice(0, w).padEnd(w);
244
+ }
245
+ var listCommand = new Command5("list").description("List all projects").action(async () => {
246
+ const { url, token } = loadOwnerSession();
247
+ const projects = await apiRequest(`${url}/projects`, token);
248
+ if (projects.length === 0) {
249
+ console.log("No projects found.");
250
+ return;
251
+ }
252
+ const header = `${col2("ID", COL_ID2)} ${col2("NAME", COL_NAME)} ${col2("ENV", COL_ENV)} FROM`;
253
+ console.log(header);
254
+ console.log("-".repeat(header.length + 30));
255
+ for (const p of projects) {
256
+ console.log(
257
+ `${col2(p.id, COL_ID2)} ${col2(p.name, COL_NAME)} ${col2(p.environment, COL_ENV)} ${p.defaultFromAddress ?? "\u2014"}`
258
+ );
259
+ }
260
+ });
261
+ var createCommand = new Command5("create").description("Create a new project").requiredOption("--name <name>", "Project name (slug, e.g. my-app)").option(
262
+ "--env <env>",
263
+ "Environment: development | staging | production",
264
+ "production"
265
+ ).option("--from <address>", "Default from address (e.g. noreply@example.com)").option("--rate-limit <n>", "Max emails per day (omit for unlimited)").option("--webhook <url>", "Webhook URL for delivery events").action(
266
+ async (opts) => {
267
+ const { url, token } = loadOwnerSession();
268
+ const project = await apiRequest(`${url}/projects`, token, {
269
+ method: "POST",
270
+ body: JSON.stringify({
271
+ name: opts.name,
272
+ environment: opts.env,
273
+ defaultFromAddress: opts.from,
274
+ rateLimitPerDay: opts.rateLimit ? Number(opts.rateLimit) : void 0,
275
+ webhookUrl: opts.webhook
276
+ })
277
+ });
278
+ console.log(`Created project: ${project.name} (${project.id})`);
279
+ }
280
+ );
281
+ var projectCommand = new Command5("project").description("Manage projects").addCommand(listCommand).addCommand(createCommand);
282
+
283
+ // src/commands/key.ts
284
+ import { Command as Command6 } from "commander";
285
+ function ownerHeaders2(token) {
286
+ return { Cookie: `session=${token}`, "Content-Type": "application/json" };
287
+ }
288
+ async function apiRequest2(url, token, init) {
289
+ const res = await fetch(url, {
290
+ ...init,
291
+ headers: { ...ownerHeaders2(token), ...init?.headers ?? {} }
292
+ });
293
+ if (!res.ok) {
294
+ const body = await res.json().catch(() => ({}));
295
+ console.error(`Error: ${body.message ?? res.statusText}`);
296
+ process.exit(1);
297
+ }
298
+ return res.json();
299
+ }
300
+ var listCommand2 = new Command6("list").description("List API keys for a project").requiredOption("--project <id>", "Project ID").action(async (opts) => {
301
+ const { url, token } = loadOwnerSession();
302
+ const keys = await apiRequest2(
303
+ `${url}/projects/${opts.project}/api-keys`,
304
+ token
305
+ );
306
+ if (keys.length === 0) {
307
+ console.log("No API keys found.");
308
+ return;
309
+ }
310
+ for (const k of keys) {
311
+ const status = k.revokedAt ? "[revoked]" : "[active]";
312
+ console.log(`${status} ${k.keyPrefix}\u2026 ${k.name} (${k.id})`);
313
+ }
314
+ });
315
+ 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(
316
+ async (opts) => {
317
+ const { url, token } = loadOwnerSession();
318
+ const result = await apiRequest2(
319
+ `${url}/projects/${opts.project}/api-keys`,
320
+ token,
321
+ {
322
+ method: "POST",
323
+ body: JSON.stringify({
324
+ name: opts.name,
325
+ expiresAt: opts.expires,
326
+ rateLimitPerMinute: opts.rateLimit ? Number(opts.rateLimit) : void 0
327
+ })
328
+ }
329
+ );
330
+ console.log(`Key name: ${result.name}`);
331
+ console.log(`Key prefix: ${result.keyPrefix}\u2026`);
332
+ console.log(`Secret: ${result.secret}`);
333
+ console.log("");
334
+ console.log("Store the secret now \u2014 it will not be shown again.");
335
+ }
336
+ );
337
+ var keyCommand = new Command6("key").description("Manage API keys").addCommand(listCommand2).addCommand(createCommand2);
338
+
339
+ // src/commands/init.ts
340
+ import { Command as Command7 } from "commander";
341
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
342
+ import { resolve } from "path";
343
+ function ownerHeaders3(token) {
344
+ return { Cookie: `session=${token}`, "Content-Type": "application/json" };
345
+ }
346
+ async function apiRequest3(url, token, init) {
347
+ const res = await fetch(url, {
348
+ ...init,
349
+ headers: { ...ownerHeaders3(token), ...init?.headers ?? {} }
350
+ });
351
+ if (!res.ok) {
352
+ const body = await res.json().catch(() => ({}));
353
+ console.error(`Error ${res.status}: ${body.message ?? res.statusText}`);
354
+ process.exit(1);
355
+ }
356
+ return res.json();
357
+ }
358
+ function upsertEnvFile(filePath, vars) {
359
+ let existing = "";
360
+ try {
361
+ existing = readFileSync2(filePath, "utf-8");
362
+ } catch {
363
+ }
364
+ const keys = Object.keys(vars);
365
+ const lines = existing.split("\n").filter((line) => !keys.some((k) => line.startsWith(`${k}=`)));
366
+ const appended = keys.map((k) => `${k}=${vars[k]}`).join("\n");
367
+ const content = lines.join("\n").trimEnd() + (lines.some((l) => l.trim()) ? "\n" : "") + appended + "\n";
368
+ writeFileSync2(filePath, content, "utf-8");
369
+ }
370
+ var initCommand = new Command7("init").description(
371
+ "Provision a project + API key and write credentials to an env file"
372
+ ).requiredOption(
373
+ "--project <name>",
374
+ "Project name (created if it does not exist)"
375
+ ).option(
376
+ "--env-file <paths>",
377
+ "Comma-separated env files to write credentials to",
378
+ ".env,.env.prod"
379
+ ).option("--key-name <name>", "Name for the new API key", "cli").option("--env <env>", "Environment for new projects", "production").option("--from <address>", "Default from address for new projects").action(
380
+ async (opts) => {
381
+ const { url, token } = loadOwnerSession();
382
+ const envFilePaths = opts.envFile.split(",").map((p) => resolve(process.cwd(), p.trim()));
383
+ const projects = await apiRequest3(`${url}/projects`, token);
384
+ let project = projects.find((p) => p.name === opts.project);
385
+ if (project) {
386
+ console.log(`Using existing project: ${project.name} (${project.id})`);
387
+ } else {
388
+ console.log(`Creating project: ${opts.project} \u2026`);
389
+ project = await apiRequest3(`${url}/projects`, token, {
390
+ method: "POST",
391
+ body: JSON.stringify({
392
+ name: opts.project,
393
+ environment: opts.env,
394
+ defaultFromAddress: opts.from
395
+ })
396
+ });
397
+ console.log(`Created: ${project.name} (${project.id})`);
398
+ }
399
+ console.log(`Creating API key "${opts.keyName}" \u2026`);
400
+ const result = await apiRequest3(
401
+ `${url}/projects/${project.id}/api-keys`,
402
+ token,
403
+ {
404
+ method: "POST",
405
+ body: JSON.stringify({ name: opts.keyName })
406
+ }
407
+ );
408
+ console.log(`Key created: ${result.keyPrefix}\u2026`);
409
+ const vars = {
410
+ DEVELEMAIL_BASE_URL: url,
411
+ DEVELEMAIL_API_KEY: result.secret
412
+ };
413
+ for (const envFilePath of envFilePaths) {
414
+ upsertEnvFile(envFilePath, vars);
415
+ console.log(`Written to ${envFilePath}`);
416
+ }
417
+ console.log("");
418
+ console.log(` DEVELEMAIL_BASE_URL=${url}`);
419
+ console.log(` DEVELEMAIL_API_KEY=${result.keyPrefix}\u2026`);
420
+ }
421
+ );
422
+
423
+ // src/index.ts
424
+ var program = new Command8().name("devel").description("DevelEmail CLI \u2014 interact with your DevelEmail instance").version("0.1.0");
425
+ program.addCommand(loginCommand);
426
+ program.addCommand(logoutCommand);
427
+ program.addCommand(projectCommand);
428
+ program.addCommand(keyCommand);
429
+ program.addCommand(initCommand);
430
+ program.addCommand(sendCommand);
431
+ program.addCommand(logsCommand);
432
+ program.addCommand(tailCommand);
433
+ program.parse(process.argv);
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@develemail/cli",
3
+ "version": "0.1.1",
4
+ "type": "module",
5
+ "bin": {
6
+ "devel": "./dist/index.js"
7
+ },
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "scripts": {
17
+ "build": "tsup"
18
+ },
19
+ "dependencies": {
20
+ "@develemail/sdk": "workspace:*",
21
+ "commander": "^12.1.0",
22
+ "dotenv": "^16.4.5"
23
+ },
24
+ "devDependencies": {
25
+ "tsup": "^8.3.5",
26
+ "typescript": "~5.7.2"
27
+ }
28
+ }