@mailmodo/cli 0.0.3-beta.pr5.8 → 0.0.3

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 (36) hide show
  1. package/dist/commands/login/index.d.ts +2 -6
  2. package/dist/commands/login/index.js +6 -62
  3. package/oclif.manifest.json +4 -559
  4. package/package.json +8 -7
  5. package/dist/commands/billing/index.d.ts +0 -26
  6. package/dist/commands/billing/index.js +0 -92
  7. package/dist/commands/contacts/index.d.ts +0 -32
  8. package/dist/commands/contacts/index.js +0 -134
  9. package/dist/commands/deploy/index.d.ts +0 -25
  10. package/dist/commands/deploy/index.js +0 -194
  11. package/dist/commands/domain/index.d.ts +0 -27
  12. package/dist/commands/domain/index.js +0 -163
  13. package/dist/commands/edit/index.d.ts +0 -14
  14. package/dist/commands/edit/index.js +0 -96
  15. package/dist/commands/emails/index.d.ts +0 -10
  16. package/dist/commands/emails/index.js +0 -62
  17. package/dist/commands/init/index.d.ts +0 -11
  18. package/dist/commands/init/index.js +0 -124
  19. package/dist/commands/logs/index.d.ts +0 -20
  20. package/dist/commands/logs/index.js +0 -82
  21. package/dist/commands/preview/index.d.ts +0 -30
  22. package/dist/commands/preview/index.js +0 -213
  23. package/dist/commands/settings/index.d.ts +0 -19
  24. package/dist/commands/settings/index.js +0 -147
  25. package/dist/commands/status/index.d.ts +0 -10
  26. package/dist/commands/status/index.js +0 -53
  27. package/dist/lib/api-client.d.ts +0 -41
  28. package/dist/lib/api-client.js +0 -125
  29. package/dist/lib/base-command.d.ts +0 -45
  30. package/dist/lib/base-command.js +0 -69
  31. package/dist/lib/config.d.ts +0 -30
  32. package/dist/lib/config.js +0 -47
  33. package/dist/lib/constants.d.ts +0 -27
  34. package/dist/lib/constants.js +0 -27
  35. package/dist/lib/yaml-config.d.ts +0 -65
  36. package/dist/lib/yaml-config.js +0 -70
@@ -1,213 +0,0 @@
1
- import { Args, Flags } from '@oclif/core';
2
- import { createServer } from 'node:http';
3
- import chalk from 'chalk';
4
- import open from 'open';
5
- import { BaseCommand } from '../../lib/base-command.js';
6
- import { API_ENDPOINTS, PREVIEW_PORT } from '../../lib/constants.js';
7
- import { loadTemplate } from '../../lib/yaml-config.js';
8
- /* eslint-disable camelcase */
9
- const SAMPLE_DATA = Object.freeze({
10
- app_url: 'https://yourapp.com',
11
- cta_url: 'https://yourapp.com/action',
12
- first_name: 'Sarah',
13
- product_name: 'YourApp',
14
- unsubscribe_url: '#',
15
- });
16
- /* eslint-enable camelcase */
17
- /**
18
- * Replaces all {{variable}} template placeholders in the HTML string
19
- * with corresponding values from the sample data map.
20
- *
21
- * @param {string} html - Raw HTML template with {{variable}} placeholders.
22
- * @param {Record<string, string>} data - Key-value map of template variable replacements.
23
- * @returns {string} HTML with all recognized placeholders replaced.
24
- */
25
- function renderTemplate(html, data) {
26
- return html.replaceAll(/\{\{(\w+)\}\}/g, (match, key) => data[key] || match);
27
- }
28
- /**
29
- * Strips HTML tags and decodes common HTML entities to produce
30
- * a readable plain text representation of an email.
31
- *
32
- * @param {string} html - The HTML content to convert.
33
- * @returns {string} Plain text with tags removed and entities decoded.
34
- */
35
- function htmlToText(html) {
36
- return html
37
- .replaceAll(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
38
- .replaceAll(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
39
- .replaceAll(/<br\s*\/?>/gi, '\n')
40
- .replaceAll(/<\/p>/gi, '\n\n')
41
- .replaceAll(/<\/div>/gi, '\n')
42
- .replaceAll(/<\/tr>/gi, '\n')
43
- .replaceAll(/<\/li>/gi, '\n')
44
- .replaceAll(/<[^>]+>/g, '')
45
- .replaceAll('&nbsp;', ' ')
46
- .replaceAll('&amp;', '&')
47
- .replaceAll('&lt;', '<')
48
- .replaceAll('&gt;', '>')
49
- .replaceAll('&quot;', '"')
50
- .replaceAll(/\n{3,}/g, '\n\n')
51
- .trim();
52
- }
53
- export default class Preview extends BaseCommand {
54
- static args = {
55
- id: Args.string({ description: 'Email ID to preview' }),
56
- };
57
- static description = 'Preview an email in browser, as text, or send a test';
58
- static examples = [
59
- '<%= config.bin %> preview welcome',
60
- '<%= config.bin %> preview welcome --text',
61
- '<%= config.bin %> preview welcome --send me@example.com',
62
- ];
63
- static flags = {
64
- ...BaseCommand.baseFlags,
65
- send: Flags.string({ description: 'Send test email to this address' }),
66
- text: Flags.boolean({ default: false, description: 'Output plain text version (for AI agents)' }),
67
- };
68
- async run() {
69
- const { args, flags } = await this.parse(Preview);
70
- const yamlConfig = await this.ensureYaml();
71
- const emailId = args.id || yamlConfig.emails[0]?.id;
72
- if (!emailId) {
73
- this.error('No emails configured. Run mailmodo init first.');
74
- }
75
- const email = yamlConfig.emails.find((e) => e.id === emailId);
76
- if (!email) {
77
- this.error(`Email '${emailId}' not found in mailmodo.yaml.`);
78
- }
79
- const sampleData = {
80
- ...SAMPLE_DATA,
81
- app_url: yamlConfig.project?.url || 'https://yourapp.com', // eslint-disable-line camelcase
82
- product_name: yamlConfig.project?.name || 'YourApp', // eslint-disable-line camelcase
83
- };
84
- if (flags.send) {
85
- await this.sendTestEmail(emailId, flags.send, flags.json);
86
- return;
87
- }
88
- const templateHtml = await loadTemplate(`${email.id}.html`);
89
- if (flags.text) {
90
- await this.renderText(email, templateHtml, sampleData, flags.json);
91
- return;
92
- }
93
- await this.startPreviewServer(email, templateHtml, sampleData, flags.json);
94
- }
95
- /**
96
- * Renders a plain text version of the email to stdout. Used by AI agents
97
- * and CI pipelines that cannot open a browser.
98
- */
99
- async renderText(email, templateHtml, sampleData, jsonOutput) {
100
- const rendered = templateHtml ? renderTemplate(templateHtml, sampleData) : '';
101
- const plainText = htmlToText(rendered);
102
- if (jsonOutput) {
103
- this.log(JSON.stringify({
104
- body: plainText,
105
- id: email.id,
106
- previewText: email.previewText,
107
- subject: email.subject,
108
- }, null, 2));
109
- return;
110
- }
111
- this.log(`\n ${chalk.bold('SUBJECT:')} ${email.subject}`);
112
- if (email.previewText) {
113
- this.log(` ${chalk.bold('PREVIEW:')} ${email.previewText}`);
114
- }
115
- this.log(`\n${plainText}\n`);
116
- }
117
- /**
118
- * Calls the API to send a test email to the specified address.
119
- * Before domain verification, tests send via the mailmodo.com domain.
120
- */
121
- async sendTestEmail(emailId, toAddress, jsonOutput) {
122
- await this.ensureAuth();
123
- const response = await this.apiClient.post(`${API_ENDPOINTS.PREVIEW}/${emailId}/send`, {
124
- to: toAddress,
125
- });
126
- if (!response.ok) {
127
- this.handleApiError(response);
128
- }
129
- if (jsonOutput) {
130
- this.log(JSON.stringify({ emailId, sentTo: toAddress, status: 'sent' }, null, 2));
131
- return;
132
- }
133
- this.log(`\n Sending test to ${chalk.cyan(toAddress)}...`);
134
- this.log(` ${chalk.dim('Note: If domain is not yet verified, test sends via mailmodo.com domain.')}`);
135
- this.log(` ${chalk.green('✓')} Sent.\n`);
136
- }
137
- /**
138
- * Starts a local HTTP server on PREVIEW_PORT to serve the rendered email
139
- * template, then opens the user's default browser to view it.
140
- */
141
- async startPreviewServer(email, templateHtml, sampleData, jsonOutput) {
142
- const rendered = templateHtml ? renderTemplate(templateHtml, sampleData) : '<p>No template found.</p>';
143
- const wrapperHtml = `<!DOCTYPE html>
144
- <html>
145
- <head>
146
- <meta charset="utf-8">
147
- <title>Preview: ${email.subject}</title>
148
- <style>
149
- body { margin: 0; padding: 2rem; background: #f5f5f5; font-family: system-ui; }
150
- .preview-bar { background: #1a1a2e; color: #fff; padding: 0.75rem 1.5rem; border-radius: 0.5rem;
151
- margin-bottom: 1.5rem; display: flex; justify-content: space-between; align-items: center; }
152
- .preview-bar h3 { margin: 0; font-size: 0.875rem; }
153
- .preview-bar span { font-size: 0.75rem; opacity: 0.7; }
154
- .email-frame { background: #fff; max-width: 40rem; margin: 0 auto; border-radius: 0.5rem;
155
- box-shadow: 0 0.125rem 0.5rem rgba(0,0,0,0.1); overflow: hidden; }
156
- .email-header { padding: 1rem 1.5rem; border-bottom: 1px solid #eee; }
157
- .email-header .subject { font-weight: 600; font-size: 1rem; }
158
- .email-header .meta { font-size: 0.75rem; color: #666; margin-top: 0.25rem; }
159
- .email-body { padding: 1.5rem; }
160
- </style>
161
- </head>
162
- <body>
163
- <div class="preview-bar">
164
- <h3>Mailmodo Preview — ${email.id}</h3>
165
- <span>Style: ${email.style || 'branded'} · Press Ctrl+C in terminal to stop</span>
166
- </div>
167
- <div class="email-frame">
168
- <div class="email-header">
169
- <div class="subject">${email.subject}</div>
170
- <div class="meta">To: ${sampleData.first_name} · From: ${sampleData.product_name}</div>
171
- </div>
172
- <div class="email-body">${rendered}</div>
173
- </div>
174
- </body>
175
- </html>`;
176
- if (jsonOutput) {
177
- this.log(JSON.stringify({
178
- id: email.id,
179
- style: email.style || 'branded',
180
- url: `http://localhost:${PREVIEW_PORT}`,
181
- }, null, 2));
182
- }
183
- const server = createServer((_req, res) => {
184
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
185
- res.end(wrapperHtml);
186
- });
187
- await new Promise((resolve) => {
188
- server.listen(PREVIEW_PORT, () => resolve());
189
- });
190
- const url = `http://localhost:${PREVIEW_PORT}`;
191
- if (!jsonOutput) {
192
- this.log(`\n Style: ${chalk.cyan(email.style || 'branded')}`);
193
- this.log(` Preview server at ${chalk.cyan(url)}`);
194
- this.log(` Opening in browser...\n`);
195
- this.log(` ${chalk.dim('Press Ctrl+C to stop the preview server.')}\n`);
196
- }
197
- try {
198
- await open(url);
199
- }
200
- catch {
201
- if (!jsonOutput) {
202
- this.log(` ${chalk.dim('Could not open browser. Visit the URL above manually.')}`);
203
- }
204
- }
205
- await new Promise((resolve) => {
206
- const shutdown = () => {
207
- server.close(() => resolve());
208
- };
209
- process.on('SIGINT', shutdown);
210
- process.on('SIGTERM', shutdown);
211
- });
212
- }
213
- }
@@ -1,19 +0,0 @@
1
- import { BaseCommand } from '../../lib/base-command.js';
2
- export default class Settings extends BaseCommand {
3
- static description: string;
4
- static examples: string[];
5
- static flags: {
6
- set: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
- json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
- yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
- };
10
- run(): Promise<void>;
11
- /**
12
- * Handles the logo file upload flow: validates the local file exists,
13
- * reads it, uploads to Mailmodo CDN via API, and updates both logoFile
14
- * and logoUrl in the project config.
15
- *
16
- * @param {import('../../lib/yaml-config.js').MailmodoYaml} yamlConfig - The full YAML config to update and save.
17
- */
18
- private handleLogoUpload;
19
- }
@@ -1,147 +0,0 @@
1
- import { Flags } from '@oclif/core';
2
- import { input, select } from '@inquirer/prompts';
3
- import chalk from 'chalk';
4
- import { existsSync } from 'node:fs';
5
- import { readFile } from 'node:fs/promises';
6
- import { resolve } from 'node:path';
7
- import { BaseCommand } from '../../lib/base-command.js';
8
- import { API_ENDPOINTS } from '../../lib/constants.js';
9
- import { saveYaml } from '../../lib/yaml-config.js';
10
- const SETTINGS_GROUPS = Object.freeze({
11
- billing: ['monthly_cap'],
12
- brand: ['email_style', 'brand_color', 'logo_url', 'logo_file'],
13
- domain: ['domain', 'address'],
14
- identity: ['from_name', 'from_email', 'reply_to'],
15
- integrations: ['webhook_url'],
16
- });
17
- /**
18
- * Converts a user-facing snake_case YAML setting key to its
19
- * corresponding camelCase TypeScript property name.
20
- *
21
- * @param {string} key - A snake_case setting key (e.g., "brand_color").
22
- * @returns {string} The camelCase property name (e.g., "brandColor").
23
- */
24
- function settingKeyToProp(key) {
25
- return key.replaceAll(/_([a-z])/g, (_, letter) => letter.toUpperCase());
26
- }
27
- export default class Settings extends BaseCommand {
28
- static description = 'View and update project settings';
29
- static examples = [
30
- '<%= config.bin %> settings',
31
- '<%= config.bin %> settings --set brand_color=#0F3460',
32
- '<%= config.bin %> settings --json',
33
- ];
34
- static flags = {
35
- ...BaseCommand.baseFlags,
36
- set: Flags.string({ description: 'Set a setting (format: key=value)' }),
37
- };
38
- async run() {
39
- const { flags } = await this.parse(Settings);
40
- const yamlConfig = await this.ensureYaml();
41
- const { project } = yamlConfig;
42
- if (flags.set) {
43
- const eqIndex = flags.set.indexOf('=');
44
- if (eqIndex === -1) {
45
- this.error('Invalid format. Use --set key=value (e.g., --set brand_color=#0F3460)');
46
- }
47
- const key = flags.set.slice(0, eqIndex).trim();
48
- const propKey = settingKeyToProp(key);
49
- const value = flags.set.slice(eqIndex + 1).trim();
50
- if (!(propKey in project)) {
51
- this.error(`Unknown setting: ${key}`);
52
- }
53
- project[propKey] = propKey === 'monthlyCap' ? Number(value) : value;
54
- await saveYaml(yamlConfig);
55
- if (flags.json) {
56
- this.log(JSON.stringify({ [propKey]: value, status: 'updated' }, null, 2));
57
- return;
58
- }
59
- this.log(`\n ${chalk.green('✓')} ${key} updated to ${chalk.cyan(value)}`);
60
- this.log(` Run ${chalk.cyan("'mailmodo deploy'")} to apply.\n`);
61
- return;
62
- }
63
- if (flags.json) {
64
- this.log(JSON.stringify({ settings: project }, null, 2));
65
- return;
66
- }
67
- this.log(`\n Current settings for ${chalk.bold(project.name || 'project')}:\n`);
68
- for (const [group, keys] of Object.entries(SETTINGS_GROUPS)) {
69
- this.log(` ${chalk.bold(group.charAt(0).toUpperCase() + group.slice(1))}`);
70
- this.log(` ${'─'.repeat(49)}`);
71
- for (const key of keys) {
72
- const propKey = settingKeyToProp(key);
73
- const value = project[propKey];
74
- const displayValue = value ? String(value) : chalk.dim('(not set)');
75
- this.log(` ${key.padEnd(16)} ${displayValue}`);
76
- }
77
- this.log('');
78
- }
79
- if (!flags.yes) {
80
- const editKey = await input({
81
- default: 'n',
82
- message: "Edit a setting? (key or 'n'):",
83
- });
84
- if (editKey !== 'n') {
85
- const editPropKey = settingKeyToProp(editKey);
86
- if (!(editPropKey in project)) {
87
- this.log(`\n Unknown setting: ${editKey}\n`);
88
- return;
89
- }
90
- if (editKey === 'logo_file') {
91
- await this.handleLogoUpload(yamlConfig);
92
- return;
93
- }
94
- if (editKey === 'email_style') {
95
- const style = await select({
96
- choices: [
97
- { name: 'plain', value: 'plain' },
98
- { name: 'branded', value: 'branded' },
99
- ],
100
- message: 'Email style:',
101
- });
102
- project.emailStyle = style;
103
- await saveYaml(yamlConfig);
104
- this.log(`\n ${chalk.green('✓')} email_style updated to ${chalk.cyan(style)}`);
105
- this.log(` Run ${chalk.cyan("'mailmodo deploy'")} to apply.\n`);
106
- return;
107
- }
108
- const newValue = await input({
109
- message: `New value for ${editKey}:`,
110
- });
111
- project[editPropKey] =
112
- editPropKey === 'monthlyCap' ? Number(newValue) : newValue;
113
- await saveYaml(yamlConfig);
114
- this.log(`\n ${chalk.green('✓')} Updated. Run ${chalk.cyan("'mailmodo deploy'")} to apply.\n`);
115
- }
116
- }
117
- }
118
- /**
119
- * Handles the logo file upload flow: validates the local file exists,
120
- * reads it, uploads to Mailmodo CDN via API, and updates both logoFile
121
- * and logoUrl in the project config.
122
- *
123
- * @param {import('../../lib/yaml-config.js').MailmodoYaml} yamlConfig - The full YAML config to update and save.
124
- */
125
- async handleLogoUpload(yamlConfig) {
126
- const logoPath = await input({ message: 'Path to logo file:' });
127
- const resolvedPath = resolve(logoPath);
128
- if (!existsSync(resolvedPath)) {
129
- this.log(`\n File not found: ${resolvedPath}\n`);
130
- return;
131
- }
132
- await this.ensureAuth();
133
- const fileBuffer = await readFile(resolvedPath);
134
- const formData = new FormData();
135
- formData.append('logo', new Blob([new Uint8Array(fileBuffer)]), logoPath.split(/[/\\]/).pop() || 'logo.png');
136
- const response = await this.apiClient.postFormData(API_ENDPOINTS.ASSETS_LOGO, formData);
137
- if (!response.ok) {
138
- this.handleApiError(response);
139
- }
140
- yamlConfig.project.logoUrl = response.data?.url || '';
141
- yamlConfig.project.logoFile = logoPath;
142
- await saveYaml(yamlConfig);
143
- this.log(`\n Logo uploaded and hosted at:`);
144
- this.log(` ${chalk.cyan(String(response.data?.url))}`);
145
- this.log(` Run ${chalk.cyan("'mailmodo deploy'")} to apply to all branded emails.\n`);
146
- }
147
- }
@@ -1,10 +0,0 @@
1
- import { BaseCommand } from '../../lib/base-command.js';
2
- export default class Status extends BaseCommand {
3
- static description: string;
4
- static examples: string[];
5
- static flags: {
6
- json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
- yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
- };
9
- run(): Promise<void>;
10
- }
@@ -1,53 +0,0 @@
1
- import chalk from 'chalk';
2
- import { BaseCommand } from '../../lib/base-command.js';
3
- import { API_ENDPOINTS } from '../../lib/constants.js';
4
- export default class Status extends BaseCommand {
5
- static description = 'View email performance metrics and quota usage';
6
- static examples = [
7
- '<%= config.bin %> status',
8
- '<%= config.bin %> status --json',
9
- ];
10
- static flags = {
11
- ...BaseCommand.baseFlags,
12
- };
13
- async run() {
14
- const { flags } = await this.parse(Status);
15
- await this.ensureAuth();
16
- const response = await this.apiClient.get(API_ENDPOINTS.ANALYTICS);
17
- if (!response.ok) {
18
- this.handleApiError(response);
19
- }
20
- const { emails, monthlySent, quota } = response.data;
21
- if (flags.json) {
22
- this.log(JSON.stringify(response.data, null, 2));
23
- return;
24
- }
25
- this.log(`\n ${chalk.bold('Last 7 days')}${''.padEnd(20)}Sent Open Click Conv`);
26
- this.log(` ${'─'.repeat(62)}`);
27
- if (emails?.length) {
28
- for (const metric of emails) {
29
- const id = (metric.emailId || '').padEnd(30);
30
- const sent = String(metric.sent ?? 0).padEnd(7);
31
- const openRate = (metric.open || '0%').padEnd(7);
32
- const clickRate = (metric.click || '0%').padEnd(8);
33
- const convRate = metric.conv || '0%';
34
- this.log(` ${id}${sent}${openRate}${clickRate}${convRate}`);
35
- }
36
- }
37
- else {
38
- this.log(` ${chalk.dim('No data yet. Deploy emails first.')}`);
39
- }
40
- this.log('');
41
- this.log(` Emails sent this month: ${chalk.bold(String(monthlySent ?? 0))}`);
42
- if (quota) {
43
- if (quota.freeRemaining > 0) {
44
- this.log(` Free tier remaining: ${chalk.cyan(String(quota.freeRemaining))}`);
45
- }
46
- else if (quota.blocksUsed !== undefined) {
47
- this.log(` Current paid block: ${chalk.cyan(`${quota.currentBlockRemaining ?? 0} / 10,000 used`)}`);
48
- this.log(` Blocks purchased: ${quota.blocksUsed}`);
49
- }
50
- }
51
- this.log('');
52
- }
53
- }
@@ -1,41 +0,0 @@
1
- export interface ApiResponse<T = Record<string, unknown>> {
2
- data: T;
3
- error?: string;
4
- ok: boolean;
5
- status: number;
6
- }
7
- /**
8
- * HTTP client for the Mailmodo CLI API.
9
- * Wraps the native fetch API with Bearer token authentication,
10
- * consistent error handling, and typed responses.
11
- *
12
- * All requests include:
13
- * - Authorization header with the user's API key
14
- * - Content-Type: application/json
15
- * - User-Agent: @mailmodo/cli
16
- *
17
- * Network errors (ECONNREFUSED, ENOTFOUND) return a friendly message
18
- * indicating the API may not be available, rather than a raw stack trace.
19
- */
20
- export declare class ApiClient {
21
- private apiKey;
22
- private baseUrl;
23
- constructor(apiKey: string);
24
- /**
25
- * Sends an HTTP request to the Mailmodo API and returns a typed response.
26
- *
27
- * @template T - The expected shape of the response body on success.
28
- * @param {string} method - HTTP method (GET, POST, PATCH, DELETE).
29
- * @param {string} path - API endpoint path relative to base URL (e.g., '/auth/validate').
30
- * @param {Record<string, unknown> | unknown} [body] - JSON-serializable request body for POST/PATCH.
31
- * @param {Record<string, string>} [params] - URL query parameters appended to the request URL.
32
- * @returns {Promise<ApiResponse<T>>} Response object with ok (boolean), status (HTTP code),
33
- * data (parsed JSON body), and optional error (string message on failure).
34
- */
35
- private request;
36
- delete<T = Record<string, unknown>>(path: string): Promise<ApiResponse<T>>;
37
- get<T = Record<string, unknown>>(path: string, params?: Record<string, string>): Promise<ApiResponse<T>>;
38
- patch<T = Record<string, unknown>>(path: string, body?: Record<string, unknown>): Promise<ApiResponse<T>>;
39
- post<T = Record<string, unknown>>(path: string, body?: Record<string, unknown> | unknown): Promise<ApiResponse<T>>;
40
- postFormData<T = Record<string, unknown>>(path: string, formData: FormData): Promise<ApiResponse<T>>;
41
- }
@@ -1,125 +0,0 @@
1
- import { API_BASE_URL } from './constants.js';
2
- /**
3
- * HTTP client for the Mailmodo CLI API.
4
- * Wraps the native fetch API with Bearer token authentication,
5
- * consistent error handling, and typed responses.
6
- *
7
- * All requests include:
8
- * - Authorization header with the user's API key
9
- * - Content-Type: application/json
10
- * - User-Agent: @mailmodo/cli
11
- *
12
- * Network errors (ECONNREFUSED, ENOTFOUND) return a friendly message
13
- * indicating the API may not be available, rather than a raw stack trace.
14
- */
15
- export class ApiClient {
16
- apiKey;
17
- baseUrl;
18
- constructor(apiKey) {
19
- this.baseUrl = API_BASE_URL;
20
- this.apiKey = apiKey;
21
- }
22
- /**
23
- * Sends an HTTP request to the Mailmodo API and returns a typed response.
24
- *
25
- * @template T - The expected shape of the response body on success.
26
- * @param {string} method - HTTP method (GET, POST, PATCH, DELETE).
27
- * @param {string} path - API endpoint path relative to base URL (e.g., '/auth/validate').
28
- * @param {Record<string, unknown> | unknown} [body] - JSON-serializable request body for POST/PATCH.
29
- * @param {Record<string, string>} [params] - URL query parameters appended to the request URL.
30
- * @returns {Promise<ApiResponse<T>>} Response object with ok (boolean), status (HTTP code),
31
- * data (parsed JSON body), and optional error (string message on failure).
32
- */
33
- async request(method, path, body, params) {
34
- const url = new URL(`${this.baseUrl}${path}`);
35
- if (params) {
36
- for (const [key, value] of Object.entries(params)) {
37
- url.searchParams.set(key, value);
38
- }
39
- }
40
- const headers = {
41
- 'Authorization': `Bearer ${this.apiKey}`,
42
- 'Content-Type': 'application/json',
43
- 'User-Agent': '@mailmodo/cli',
44
- };
45
- try {
46
- const response = await fetch(url.toString(), {
47
- body: body ? JSON.stringify(body) : undefined,
48
- headers,
49
- method,
50
- });
51
- const data = await response.json().catch(() => ({}));
52
- if (!response.ok) {
53
- const errorData = data;
54
- return {
55
- data: data,
56
- error: errorData?.message ||
57
- errorData?.error ||
58
- `Request failed with status ${response.status}`,
59
- ok: false,
60
- status: response.status,
61
- };
62
- }
63
- return { data: data, ok: true, status: response.status };
64
- }
65
- catch (error) {
66
- const err = error;
67
- const isConnectionError = err?.cause?.code === 'ECONNREFUSED' || err?.cause?.code === 'ENOTFOUND';
68
- return {
69
- data: {},
70
- error: isConnectionError
71
- ? 'Cannot connect to Mailmodo API. The API service may not be available yet.'
72
- : (err?.message || 'An unexpected network error occurred.'),
73
- ok: false,
74
- status: 0,
75
- };
76
- }
77
- }
78
- async delete(path) {
79
- return this.request('DELETE', path);
80
- }
81
- async get(path, params) {
82
- return this.request('GET', path, undefined, params);
83
- }
84
- async patch(path, body) {
85
- return this.request('PATCH', path, body);
86
- }
87
- async post(path, body) {
88
- return this.request('POST', path, body);
89
- }
90
- async postFormData(path, formData) {
91
- const url = new URL(`${this.baseUrl}${path}`);
92
- try {
93
- const response = await fetch(url.toString(), {
94
- body: formData,
95
- headers: {
96
- 'Authorization': `Bearer ${this.apiKey}`,
97
- 'User-Agent': '@mailmodo/cli',
98
- },
99
- method: 'POST',
100
- });
101
- const data = await response.json().catch(() => ({}));
102
- if (!response.ok) {
103
- const errorData = data;
104
- return {
105
- data: data,
106
- error: errorData?.message ||
107
- errorData?.error ||
108
- `Upload failed with status ${response.status}`,
109
- ok: false,
110
- status: response.status,
111
- };
112
- }
113
- return { data: data, ok: true, status: response.status };
114
- }
115
- catch (error) {
116
- const err = error;
117
- return {
118
- data: {},
119
- error: err?.message || 'File upload failed.',
120
- ok: false,
121
- status: 0,
122
- };
123
- }
124
- }
125
- }
@@ -1,45 +0,0 @@
1
- import { Command } from '@oclif/core';
2
- import { ApiClient } from './api-client.js';
3
- import { type MailmodoConfig } from './config.js';
4
- import { type MailmodoYaml } from './yaml-config.js';
5
- /**
6
- * Abstract base command providing shared functionality for all Mailmodo CLI commands.
7
- * Subclasses inherit --json and --yes base flags, authentication enforcement,
8
- * YAML config loading, and consistent API error handling.
9
- */
10
- export declare abstract class BaseCommand extends Command {
11
- static baseFlags: {
12
- json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
- yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
- };
15
- protected apiClient: ApiClient;
16
- /**
17
- * Validates that the user is authenticated and initializes the API client.
18
- * Checks MAILMODO_API_KEY environment variable first (for AI agent usage),
19
- * then falls back to ~/.mailmodo/config.
20
- * Exits with an error if no valid API key is found.
21
- *
22
- * @returns {Promise<MailmodoConfig>} The resolved configuration containing the API key.
23
- */
24
- protected ensureAuth(): Promise<MailmodoConfig>;
25
- /**
26
- * Loads and returns the mailmodo.yaml configuration from the current directory.
27
- * Exits with an error if the file is not found, directing the user to run init.
28
- *
29
- * @returns {Promise<MailmodoYaml>} The parsed mailmodo.yaml containing project
30
- * settings and all email sequence definitions.
31
- */
32
- protected ensureYaml(): Promise<MailmodoYaml>;
33
- /**
34
- * Handles a failed API response by mapping HTTP status codes to
35
- * user-friendly error messages and exiting the process.
36
- *
37
- * @param {{ status: number; error?: string }} response - The API response object with ok=false.
38
- * Status 401 prompts re-authentication, 429 indicates rate limiting,
39
- * all others display the server's error message or a generic fallback.
40
- */
41
- protected handleApiError(response: {
42
- error?: string;
43
- status: number;
44
- }): never;
45
- }