@mailmodo/cli 0.0.49 → 0.0.50-beta.pr52.80
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/dist/commands/deploy/index.d.ts +18 -0
- package/dist/commands/deploy/index.js +76 -2
- package/dist/commands/deployments/index.d.ts +14 -0
- package/dist/commands/deployments/index.js +76 -0
- package/dist/commands/edit/index.js +1 -0
- package/dist/commands/init/index.d.ts +1 -0
- package/dist/commands/init/index.js +10 -6
- package/dist/lib/base-command.d.ts +30 -3
- package/dist/lib/base-command.js +38 -6
- package/dist/lib/constants.d.ts +6 -0
- package/dist/lib/constants.js +6 -0
- package/dist/lib/messages.d.ts +33 -0
- package/dist/lib/messages.js +71 -0
- package/oclif.manifest.json +125 -64
- package/package.json +1 -1
|
@@ -3,12 +3,30 @@ export default class Deploy extends BaseCommand {
|
|
|
3
3
|
static description: string;
|
|
4
4
|
static examples: string[];
|
|
5
5
|
static flags: {
|
|
6
|
+
pause: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
resume: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
8
|
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
9
|
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
10
|
};
|
|
9
11
|
private fetchDomainVerifyForDeploy;
|
|
10
12
|
run(): Promise<void>;
|
|
11
13
|
private validateSequence;
|
|
14
|
+
/**
|
|
15
|
+
* Calls `POST /sequences/{id}/status` with `{ status: "paused" }` and prints
|
|
16
|
+
* a confirmation-aware success/no-op message. Skips the prompt entirely when
|
|
17
|
+
* `--yes` is set so the command stays scriptable. `--json` always emits the
|
|
18
|
+
* raw server payload (sequenceId, status, alreadyInStatus).
|
|
19
|
+
*/
|
|
20
|
+
private pauseSequence;
|
|
21
|
+
/**
|
|
22
|
+
* Calls `POST /sequences/{id}/status` with `{ status: "active" }`. No prompt
|
|
23
|
+
* — resuming is the safe direction (it does not start sends that weren't
|
|
24
|
+
* already queued). `--json` always emits the raw server payload (sequenceId,
|
|
25
|
+
* status, alreadyInStatus).
|
|
26
|
+
*/
|
|
27
|
+
private resumeSequence;
|
|
28
|
+
private updateSequenceStatus;
|
|
29
|
+
private sequenceStatusPath;
|
|
12
30
|
private buildDeployPayload;
|
|
13
31
|
private resolveMonthlyCapForDeploy;
|
|
14
32
|
private mapEmailToPayload;
|
|
@@ -1,17 +1,28 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
1
2
|
import { confirm, input } from '@inquirer/prompts';
|
|
2
3
|
import chalk from 'chalk';
|
|
3
4
|
import { BaseCommand } from '../../lib/base-command.js';
|
|
4
5
|
import { API_ENDPOINTS, DEFAULT_BRAND_COLOR } from '../../lib/constants.js';
|
|
5
|
-
import { ERRORS, INFO, PROMPTS, SEPARATOR } from '../../lib/messages.js';
|
|
6
|
+
import { ERRORS, INFO, pauseAlready, pauseSuccess, PROMPTS, resumeAlready, resumeSuccess, SEPARATOR, } from '../../lib/messages.js';
|
|
6
7
|
import { loadTemplate, } from '../../lib/yaml-config.js';
|
|
7
8
|
export default class Deploy extends BaseCommand {
|
|
8
|
-
static description = 'Deploy
|
|
9
|
+
static description = 'Deploy, pause, or resume an email sequence';
|
|
9
10
|
static examples = [
|
|
10
11
|
'<%= config.bin %> deploy',
|
|
11
12
|
'<%= config.bin %> deploy --yes',
|
|
13
|
+
'<%= config.bin %> deploy --pause seq_abc123',
|
|
14
|
+
'<%= config.bin %> deploy --resume seq_abc123 --json',
|
|
12
15
|
];
|
|
13
16
|
static flags = {
|
|
14
17
|
...BaseCommand.baseFlags,
|
|
18
|
+
pause: Flags.string({
|
|
19
|
+
description: 'Pause a deployed sequence by ID (stops scheduled + triggered sends)',
|
|
20
|
+
exclusive: ['resume'],
|
|
21
|
+
}),
|
|
22
|
+
resume: Flags.string({
|
|
23
|
+
description: 'Resume a paused sequence by ID',
|
|
24
|
+
exclusive: ['pause'],
|
|
25
|
+
}),
|
|
15
26
|
};
|
|
16
27
|
fetchDomainVerifyForDeploy(jsonOutput, domain) {
|
|
17
28
|
return this.withApiSpinner({ json: jsonOutput, text: ' Checking domain verification...' }, () => this.apiClient.get(API_ENDPOINTS.DOMAIN_VERIFY, {
|
|
@@ -21,6 +32,15 @@ export default class Deploy extends BaseCommand {
|
|
|
21
32
|
async run() {
|
|
22
33
|
const { flags } = await this.parse(Deploy);
|
|
23
34
|
await this.ensureAuth();
|
|
35
|
+
const baseFlags = { json: flags.json, yes: flags.yes };
|
|
36
|
+
if (flags.pause) {
|
|
37
|
+
await this.pauseSequence(flags.pause, baseFlags);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (flags.resume) {
|
|
41
|
+
await this.resumeSequence(flags.resume, baseFlags);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
24
44
|
const yamlConfig = await this.ensureYaml();
|
|
25
45
|
const domainReady = await this.ensureDomainReady(yamlConfig, flags);
|
|
26
46
|
if (!domainReady)
|
|
@@ -60,6 +80,60 @@ export default class Deploy extends BaseCommand {
|
|
|
60
80
|
}
|
|
61
81
|
return response.data;
|
|
62
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Calls `POST /sequences/{id}/status` with `{ status: "paused" }` and prints
|
|
85
|
+
* a confirmation-aware success/no-op message. Skips the prompt entirely when
|
|
86
|
+
* `--yes` is set so the command stays scriptable. `--json` always emits the
|
|
87
|
+
* raw server payload (sequenceId, status, alreadyInStatus).
|
|
88
|
+
*/
|
|
89
|
+
async pauseSequence(sequenceId, flags) {
|
|
90
|
+
if (!flags.yes) {
|
|
91
|
+
const confirmed = await confirm({
|
|
92
|
+
default: false,
|
|
93
|
+
message: PROMPTS.PAUSE_CONFIRM,
|
|
94
|
+
});
|
|
95
|
+
if (!confirmed) {
|
|
96
|
+
this.log(`\n ${INFO.PAUSE_CANCELLED}\n`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const data = await this.updateSequenceStatus(sequenceId, 'paused', flags, ' Pausing sequence...');
|
|
101
|
+
if (flags.json) {
|
|
102
|
+
this.log(JSON.stringify(data, null, 2));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const message = data.alreadyInStatus
|
|
106
|
+
? pauseAlready(data.sequenceId || sequenceId)
|
|
107
|
+
: pauseSuccess(data.sequenceId || sequenceId);
|
|
108
|
+
this.log(`\n ${message}\n`);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Calls `POST /sequences/{id}/status` with `{ status: "active" }`. No prompt
|
|
112
|
+
* — resuming is the safe direction (it does not start sends that weren't
|
|
113
|
+
* already queued). `--json` always emits the raw server payload (sequenceId,
|
|
114
|
+
* status, alreadyInStatus).
|
|
115
|
+
*/
|
|
116
|
+
async resumeSequence(sequenceId, flags) {
|
|
117
|
+
const data = await this.updateSequenceStatus(sequenceId, 'active', flags, ' Resuming sequence...');
|
|
118
|
+
if (flags.json) {
|
|
119
|
+
this.log(JSON.stringify(data, null, 2));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const message = data.alreadyInStatus
|
|
123
|
+
? resumeAlready(data.sequenceId || sequenceId)
|
|
124
|
+
: resumeSuccess(data.sequenceId || sequenceId);
|
|
125
|
+
this.log(`\n ${message}\n`);
|
|
126
|
+
}
|
|
127
|
+
async updateSequenceStatus(sequenceId, status, flags, spinnerText) {
|
|
128
|
+
const response = await this.withApiSpinner({ json: flags.json, text: spinnerText }, () => this.apiClient.post(this.sequenceStatusPath(sequenceId), { status }));
|
|
129
|
+
if (!response.ok) {
|
|
130
|
+
this.handleApiError(response);
|
|
131
|
+
}
|
|
132
|
+
return response.data;
|
|
133
|
+
}
|
|
134
|
+
sequenceStatusPath(sequenceId) {
|
|
135
|
+
return `${API_ENDPOINTS.SEQUENCES}/${encodeURIComponent(sequenceId)}/status`;
|
|
136
|
+
}
|
|
63
137
|
async buildDeployPayload(yamlConfig) {
|
|
64
138
|
const [emailsWithHtml, monthlyCap] = await Promise.all([
|
|
65
139
|
Promise.all(yamlConfig.emails.map(async (email) => {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { BaseCommand } from '../../lib/base-command.js';
|
|
2
|
+
export default class Deployments 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
|
+
private renderTable;
|
|
11
|
+
private statusColor;
|
|
12
|
+
private colWidth;
|
|
13
|
+
private formatDate;
|
|
14
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
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 Deployments extends BaseCommand {
|
|
5
|
+
static description = 'List every deployed sequence on this account, with the IDs needed for deploy --pause / --resume';
|
|
6
|
+
static examples = [
|
|
7
|
+
'<%= config.bin %> deployments',
|
|
8
|
+
'<%= config.bin %> deployments --json',
|
|
9
|
+
];
|
|
10
|
+
static flags = {
|
|
11
|
+
...BaseCommand.baseFlags,
|
|
12
|
+
};
|
|
13
|
+
async run() {
|
|
14
|
+
const { flags } = await this.parse(Deployments);
|
|
15
|
+
await this.ensureAuth();
|
|
16
|
+
const response = await this.withApiSpinner({ json: flags.json, text: ' Loading deployments...' }, () => this.apiClient.get(API_ENDPOINTS.SEQUENCES));
|
|
17
|
+
if (!response.ok) {
|
|
18
|
+
this.handleApiError(response);
|
|
19
|
+
}
|
|
20
|
+
if (flags.json) {
|
|
21
|
+
this.log(JSON.stringify(response.data, null, 2));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
this.renderTable(response.data);
|
|
25
|
+
}
|
|
26
|
+
renderTable(data) {
|
|
27
|
+
const sequences = data.sequences ?? [];
|
|
28
|
+
if (sequences.length === 0) {
|
|
29
|
+
this.log(`\n ${chalk.dim('No deployed sequences yet.')}`);
|
|
30
|
+
this.log(` Run ${chalk.cyan('mailmodo deploy')} to deploy one.\n`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const rows = sequences.map((seq) => ({
|
|
34
|
+
emails: String(seq.emailCount ?? 0),
|
|
35
|
+
product: seq.productName ?? '',
|
|
36
|
+
sequenceId: seq.sequenceId ?? '',
|
|
37
|
+
status: seq.status ?? '',
|
|
38
|
+
updated: this.formatDate(seq.updatedAt),
|
|
39
|
+
}));
|
|
40
|
+
const widths = {
|
|
41
|
+
emails: this.colWidth(rows, 'emails', 'Emails'),
|
|
42
|
+
product: this.colWidth(rows, 'product', 'Product'),
|
|
43
|
+
sequenceId: this.colWidth(rows, 'sequenceId', 'Sequence ID'),
|
|
44
|
+
status: this.colWidth(rows, 'status', 'Status'),
|
|
45
|
+
updated: this.colWidth(rows, 'updated', 'Updated'),
|
|
46
|
+
};
|
|
47
|
+
this.log(`\n ${chalk.bold(String(sequences.length))} deployed ${sequences.length === 1 ? 'sequence' : 'sequences'}:\n`);
|
|
48
|
+
this.log(` ${chalk.bold('Product'.padEnd(widths.product))}${chalk.bold('Status'.padEnd(widths.status))}${chalk.bold('Emails'.padEnd(widths.emails))}${chalk.bold('Sequence ID'.padEnd(widths.sequenceId))}${chalk.bold('Updated')}`);
|
|
49
|
+
this.log(` ${'─'.repeat(widths.product + widths.status + widths.emails + widths.sequenceId + widths.updated)}`);
|
|
50
|
+
for (const row of rows) {
|
|
51
|
+
const status = this.statusColor(row.status)(row.status.padEnd(widths.status));
|
|
52
|
+
this.log(` ${row.product.padEnd(widths.product)}${status}${row.emails.padEnd(widths.emails)}${chalk.cyan(row.sequenceId.padEnd(widths.sequenceId))}${chalk.dim(row.updated)}`);
|
|
53
|
+
}
|
|
54
|
+
this.log('');
|
|
55
|
+
this.log(` Pause: ${chalk.cyan('mailmodo deploy --pause <sequence-id>')}`);
|
|
56
|
+
this.log(` Resume: ${chalk.cyan('mailmodo deploy --resume <sequence-id>')}\n`);
|
|
57
|
+
}
|
|
58
|
+
statusColor(status) {
|
|
59
|
+
if (status === 'active')
|
|
60
|
+
return chalk.green;
|
|
61
|
+
if (status === 'paused')
|
|
62
|
+
return chalk.yellow;
|
|
63
|
+
return chalk.white;
|
|
64
|
+
}
|
|
65
|
+
colWidth(rows, key, header) {
|
|
66
|
+
return Math.max(...rows.map((r) => r[key].length), header.length) + 2;
|
|
67
|
+
}
|
|
68
|
+
formatDate(iso) {
|
|
69
|
+
if (!iso)
|
|
70
|
+
return '';
|
|
71
|
+
const parsed = new Date(iso);
|
|
72
|
+
if (Number.isNaN(parsed.getTime()))
|
|
73
|
+
return iso;
|
|
74
|
+
return parsed.toISOString().slice(0, 10);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -49,6 +49,7 @@ export default class Edit extends BaseCommand {
|
|
|
49
49
|
async runEditStep(ctx, changeDescription, flags) {
|
|
50
50
|
const response = await this.withApiSpinner({ json: flags.json, text: ' Applying AI edits...' }, () => this.callEditApi(changeDescription, ctx.email, ctx.templateHtml));
|
|
51
51
|
if (!response.ok) {
|
|
52
|
+
this.handleAiQuotaError(response, 'edit');
|
|
52
53
|
this.handleApiError(response);
|
|
53
54
|
}
|
|
54
55
|
const updated = response.data;
|
|
@@ -64,6 +64,7 @@ export default class Init extends BaseCommand {
|
|
|
64
64
|
url: productUrl,
|
|
65
65
|
}));
|
|
66
66
|
if (!analysisResponse.ok) {
|
|
67
|
+
this.handleAiQuotaError(analysisResponse, 'init');
|
|
67
68
|
this.handleApiError(analysisResponse);
|
|
68
69
|
}
|
|
69
70
|
const analysis = analysisResponse.data;
|
|
@@ -154,12 +155,7 @@ export default class Init extends BaseCommand {
|
|
|
154
155
|
},
|
|
155
156
|
};
|
|
156
157
|
await saveYaml(yamlConfig);
|
|
157
|
-
|
|
158
|
-
const monthlyCap = billingStatus?.cap?.inBlocks;
|
|
159
|
-
if (monthlyCap !== null && monthlyCap !== undefined) {
|
|
160
|
-
yamlConfig.project.monthlyCap = monthlyCap;
|
|
161
|
-
await saveYaml(yamlConfig);
|
|
162
|
-
}
|
|
158
|
+
await this.persistMonthlyCap(yamlConfig);
|
|
163
159
|
const templateSaves = analysisPayload.recommendedEmails.flatMap((rec, index) => {
|
|
164
160
|
const generated = generatedEmails[index];
|
|
165
161
|
const saves = [];
|
|
@@ -184,6 +180,14 @@ export default class Init extends BaseCommand {
|
|
|
184
180
|
this.log(` Created ${chalk.green('mailmodo.yaml')} + ${chalk.green(String(emailConfigs.length))} email templates in ${chalk.green('/mailmodo')}\n`);
|
|
185
181
|
this.log(` Run ${chalk.cyan("'mailmodo emails'")} to review.\n`);
|
|
186
182
|
}
|
|
183
|
+
async persistMonthlyCap(yamlConfig) {
|
|
184
|
+
const billingStatus = await this.fetchBillingStatus();
|
|
185
|
+
const monthlyCap = billingStatus?.cap?.inBlocks;
|
|
186
|
+
if (monthlyCap !== null && monthlyCap !== undefined) {
|
|
187
|
+
yamlConfig.project.monthlyCap = monthlyCap;
|
|
188
|
+
await saveYaml(yamlConfig);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
187
191
|
async confirmOverwriteIfNeeded(flags) {
|
|
188
192
|
const existing = await loadYaml();
|
|
189
193
|
if (!existing)
|
|
@@ -2,6 +2,11 @@ import { Command } from '@oclif/core';
|
|
|
2
2
|
import { ApiClient, type ApiRequestDebugInfo } from './api-client.js';
|
|
3
3
|
import { type MailmodoConfig } from './config.js';
|
|
4
4
|
import { type MailmodoYaml } from './yaml-config.js';
|
|
5
|
+
/**
|
|
6
|
+
* Kind of AI quota that was exhausted. Drives both the headline default
|
|
7
|
+
* message and the follow-up tip pointing the user at the next-best action.
|
|
8
|
+
*/
|
|
9
|
+
export type AiQuotaFeature = 'edit' | 'init';
|
|
5
10
|
export interface BillingCapUpdateResult {
|
|
6
11
|
autoChargeBlockCount?: number;
|
|
7
12
|
capBlocks: number;
|
|
@@ -76,6 +81,25 @@ export declare abstract class BaseCommand extends Command {
|
|
|
76
81
|
error?: string;
|
|
77
82
|
status: number;
|
|
78
83
|
}): never;
|
|
84
|
+
/**
|
|
85
|
+
* Intercepts a 429 "Monthly quota exhausted" response from an AI rate-limited
|
|
86
|
+
* endpoint (analyze for `init`, email edit for `edit`) and exits with a
|
|
87
|
+
* feature-specific message that includes the server's error text, the reset
|
|
88
|
+
* date, optional retry hint, and the next-best action. Returns without
|
|
89
|
+
* exiting when the response is not a 429 so callers can fall through to
|
|
90
|
+
* `handleApiError` for non-quota failures.
|
|
91
|
+
*
|
|
92
|
+
* @param {{ status: number; error?: string; data?: unknown; debug?: ApiRequestDebugInfo }} response - The failed API response.
|
|
93
|
+
* Must carry the parsed JSON body (`data`) so reset/retry fields can be surfaced.
|
|
94
|
+
* @param {AiQuotaFeature} feature - Which AI quota was hit. Drives the default headline + tip
|
|
95
|
+
* shown when the server omits an explicit `error` string.
|
|
96
|
+
*/
|
|
97
|
+
protected handleAiQuotaError(response: {
|
|
98
|
+
data?: unknown;
|
|
99
|
+
debug?: ApiRequestDebugInfo;
|
|
100
|
+
error?: string;
|
|
101
|
+
status: number;
|
|
102
|
+
}, feature: AiQuotaFeature): void;
|
|
79
103
|
protected collectDomainSetupInputs(yamlConfig: MailmodoYaml, skipPrompts: boolean): Promise<{
|
|
80
104
|
address: string;
|
|
81
105
|
domain: string;
|
|
@@ -144,12 +168,15 @@ export declare abstract class BaseCommand extends Command {
|
|
|
144
168
|
value: string;
|
|
145
169
|
}>, guideUrl: string | undefined, json: boolean): void;
|
|
146
170
|
/**
|
|
147
|
-
* Builds the terminal error string for a failed API call
|
|
148
|
-
*
|
|
171
|
+
* Builds the terminal error string for a failed API call. In production only
|
|
172
|
+
* the user-facing message is returned; verbose request diagnostics (URL,
|
|
173
|
+
* status, response body, error code) are appended only when [[IS_DEV_MODE]]
|
|
174
|
+
* is true so end users never see internal request metadata.
|
|
149
175
|
*
|
|
150
176
|
* @param {string} message - Primary error text (HTTP message or generic).
|
|
151
177
|
* @param {{ status: number; debug?: ApiRequestDebugInfo }} response - Failed API response.
|
|
152
|
-
* @returns {string} Message
|
|
178
|
+
* @returns {string} Message alone in production, or message plus indented
|
|
179
|
+
* Request details when running in dev/debug mode.
|
|
153
180
|
*/
|
|
154
181
|
private formatApiFailure;
|
|
155
182
|
}
|
package/dist/lib/base-command.js
CHANGED
|
@@ -4,8 +4,8 @@ import chalk from 'chalk';
|
|
|
4
4
|
import ora from 'ora';
|
|
5
5
|
import { ApiClient } from './api-client.js';
|
|
6
6
|
import { loadConfig } from './config.js';
|
|
7
|
-
import { API_ENDPOINTS } from './constants.js';
|
|
8
|
-
import { ERRORS, INFO, PROMPTS, recordLabel, VALIDATION } from './messages.js';
|
|
7
|
+
import { API_ENDPOINTS, IS_DEV_MODE } from './constants.js';
|
|
8
|
+
import { ERRORS, INFO, PROMPTS, quotaExhaustedMessage, recordLabel, VALIDATION, } from './messages.js';
|
|
9
9
|
import { loadYaml, saveYaml } from './yaml-config.js';
|
|
10
10
|
export const FREE_TIER = 'free';
|
|
11
11
|
/**
|
|
@@ -107,6 +107,35 @@ export class BaseCommand extends Command {
|
|
|
107
107
|
}
|
|
108
108
|
this.error(this.formatApiFailure(response.error || ERRORS.UNEXPECTED_API, response));
|
|
109
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Intercepts a 429 "Monthly quota exhausted" response from an AI rate-limited
|
|
112
|
+
* endpoint (analyze for `init`, email edit for `edit`) and exits with a
|
|
113
|
+
* feature-specific message that includes the server's error text, the reset
|
|
114
|
+
* date, optional retry hint, and the next-best action. Returns without
|
|
115
|
+
* exiting when the response is not a 429 so callers can fall through to
|
|
116
|
+
* `handleApiError` for non-quota failures.
|
|
117
|
+
*
|
|
118
|
+
* @param {{ status: number; error?: string; data?: unknown; debug?: ApiRequestDebugInfo }} response - The failed API response.
|
|
119
|
+
* Must carry the parsed JSON body (`data`) so reset/retry fields can be surfaced.
|
|
120
|
+
* @param {AiQuotaFeature} feature - Which AI quota was hit. Drives the default headline + tip
|
|
121
|
+
* shown when the server omits an explicit `error` string.
|
|
122
|
+
*/
|
|
123
|
+
handleAiQuotaError(response, feature) {
|
|
124
|
+
if (response.status !== 429)
|
|
125
|
+
return;
|
|
126
|
+
const body = (response.data ?? {});
|
|
127
|
+
const isInit = feature === 'init';
|
|
128
|
+
const message = quotaExhaustedMessage({
|
|
129
|
+
defaultMessage: isInit
|
|
130
|
+
? ERRORS.QUOTA_INIT_DEFAULT
|
|
131
|
+
: ERRORS.QUOTA_EDIT_DEFAULT,
|
|
132
|
+
limitReset: body.limitReset,
|
|
133
|
+
retryAfter: body.retryAfter,
|
|
134
|
+
serverError: typeof body.error === 'string' ? body.error : undefined,
|
|
135
|
+
tip: isInit ? ERRORS.QUOTA_INIT_TIP : ERRORS.QUOTA_EDIT_TIP,
|
|
136
|
+
});
|
|
137
|
+
this.error(this.formatApiFailure(message, response));
|
|
138
|
+
}
|
|
110
139
|
async collectDomainSetupInputs(yamlConfig, skipPrompts) {
|
|
111
140
|
if (skipPrompts) {
|
|
112
141
|
const domain = yamlConfig.project?.domain || '';
|
|
@@ -267,16 +296,19 @@ export class BaseCommand extends Command {
|
|
|
267
296
|
this.log(` Full guide: ${chalk.cyan(guideUrl)}\n`);
|
|
268
297
|
}
|
|
269
298
|
/**
|
|
270
|
-
* Builds the terminal error string for a failed API call
|
|
271
|
-
*
|
|
299
|
+
* Builds the terminal error string for a failed API call. In production only
|
|
300
|
+
* the user-facing message is returned; verbose request diagnostics (URL,
|
|
301
|
+
* status, response body, error code) are appended only when [[IS_DEV_MODE]]
|
|
302
|
+
* is true so end users never see internal request metadata.
|
|
272
303
|
*
|
|
273
304
|
* @param {string} message - Primary error text (HTTP message or generic).
|
|
274
305
|
* @param {{ status: number; debug?: ApiRequestDebugInfo }} response - Failed API response.
|
|
275
|
-
* @returns {string} Message
|
|
306
|
+
* @returns {string} Message alone in production, or message plus indented
|
|
307
|
+
* Request details when running in dev/debug mode.
|
|
276
308
|
*/
|
|
277
309
|
formatApiFailure(message, response) {
|
|
278
310
|
const { debug, status } = response;
|
|
279
|
-
if (!debug) {
|
|
311
|
+
if (!IS_DEV_MODE || !debug) {
|
|
280
312
|
return message;
|
|
281
313
|
}
|
|
282
314
|
return [
|
package/dist/lib/constants.d.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* True when the CLI is running in a dev/debug context. Verbose request
|
|
3
|
+
* diagnostics (URL, status, response body, error code) are only surfaced
|
|
4
|
+
* when this is true so end users never see internal request metadata.
|
|
5
|
+
*/
|
|
6
|
+
export declare const IS_DEV_MODE: boolean;
|
|
1
7
|
export declare const API_BASE_URL = "https://app-vertex-debug.azurewebsites.net";
|
|
2
8
|
export declare const API_ENDPOINTS: Readonly<{
|
|
3
9
|
ANALYTICS: "/analytics";
|
package/dist/lib/constants.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
/** Set by `bin/dev.js` when running the CLI locally (tsx bootstrap). */
|
|
2
2
|
const DEV_API_BASE_URL = 'https://app-vertex-debug.azurewebsites.net';
|
|
3
|
+
/**
|
|
4
|
+
* True when the CLI is running in a dev/debug context. Verbose request
|
|
5
|
+
* diagnostics (URL, status, response body, error code) are only surfaced
|
|
6
|
+
* when this is true so end users never see internal request metadata.
|
|
7
|
+
*/
|
|
8
|
+
export const IS_DEV_MODE = Boolean(process.env.MAILMODO_DEV_TSX || process.env.MAILMODO_DEBUG);
|
|
3
9
|
// const PRODUCTION_API_BASE_URL = 'https://api.mailmodo.com';
|
|
4
10
|
const PRODUCTION_API_BASE_URL = 'https://app-vertex-debug.azurewebsites.net';
|
|
5
11
|
export const API_BASE_URL = process.env.MAILMODO_DEV_TSX
|
package/dist/lib/messages.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export declare const PROMPTS: {
|
|
|
9
9
|
readonly DOMAIN: "What domain will you send from?";
|
|
10
10
|
readonly ENTER_AFTER_RECORDS: "Press Enter once you've added the records, or 'skip'.";
|
|
11
11
|
readonly FROM_NAME: "Display name (optional, shown as sender name):";
|
|
12
|
+
readonly PAUSE_CONFIRM: "Pause this sequence? All scheduled and triggered sends will stop until you resume.";
|
|
12
13
|
readonly REPLY_TO: "Reply-to address (optional, press Enter to use sender email):";
|
|
13
14
|
readonly SENDER_EMAIL: "Sender email address:";
|
|
14
15
|
};
|
|
@@ -19,6 +20,10 @@ export declare const ERRORS: {
|
|
|
19
20
|
readonly INVALID_API_KEY: `Invalid API key. Run ${string} to re-authenticate.`;
|
|
20
21
|
readonly NOT_LOGGED_IN: `Not logged in. Run ${string} to authenticate.`;
|
|
21
22
|
readonly NO_YAML: `No mailmodo.yaml found. Run ${string} first.`;
|
|
23
|
+
readonly QUOTA_EDIT_DEFAULT: "Edit limit reached for this month.";
|
|
24
|
+
readonly QUOTA_EDIT_TIP: "AI edits are limited to 50 per account per month. Manually edit the template file under ./mailmodo to make further changes.";
|
|
25
|
+
readonly QUOTA_INIT_DEFAULT: "Regeneration limit reached.";
|
|
26
|
+
readonly QUOTA_INIT_TIP: `Regenerations are limited to 5 per account per month. Run ${string} to modify individual emails instead.`;
|
|
22
27
|
readonly RATE_LIMIT: "Rate limit exceeded. Please try again later.";
|
|
23
28
|
readonly UNEXPECTED_API: "An unexpected API error occurred.";
|
|
24
29
|
};
|
|
@@ -33,7 +38,35 @@ export declare const INFO: {
|
|
|
33
38
|
Then: ${string}`;
|
|
34
39
|
readonly DOMAIN_PENDING_VERIFICATION: `Your domain is not verified yet. Please verify it first. Run ${string} to check the status.`;
|
|
35
40
|
readonly FREE_TIER_CAP_BLOCKED: `Monthly cap is a paid-tier setting and is not available on the free tier. Run ${string} to add a payment method, then set a cap.`;
|
|
41
|
+
readonly PAUSE_CANCELLED: "Pause cancelled. Sequence is still live.";
|
|
36
42
|
readonly SEQUENCES_NOT_DEPLOYED: `Sequences saved but ${string}.`;
|
|
37
43
|
};
|
|
44
|
+
export declare function pauseSuccess(sequenceId: string): string;
|
|
45
|
+
export declare function pauseAlready(sequenceId: string): string;
|
|
46
|
+
export declare function resumeSuccess(sequenceId: string): string;
|
|
47
|
+
export declare function resumeAlready(sequenceId: string): string;
|
|
38
48
|
export declare function yamlParseError(detail: string): string;
|
|
39
49
|
export declare function recordLabel(index: number): string;
|
|
50
|
+
/**
|
|
51
|
+
* Parses an ISO-8601 timestamp into a `YYYY-MM-DD` date string.
|
|
52
|
+
* Returns the raw input if parsing fails so the original value is preserved.
|
|
53
|
+
*/
|
|
54
|
+
export declare function formatQuotaResetDate(value?: string): string | undefined;
|
|
55
|
+
/**
|
|
56
|
+
* Formats a `retry-after` value (seconds) into a short, human-readable hint
|
|
57
|
+
* suitable for appending to a 429 error message.
|
|
58
|
+
*/
|
|
59
|
+
export declare function formatRetryAfter(seconds?: number): string | undefined;
|
|
60
|
+
/**
|
|
61
|
+
* Builds the multi-line message printed when an AI quota (init regeneration or
|
|
62
|
+
* edit) is exhausted. `serverError` is the message returned by the API; the
|
|
63
|
+
* default text is used when the server omits it. The tip lists the next action
|
|
64
|
+
* the user can take (e.g. switch from init to edit).
|
|
65
|
+
*/
|
|
66
|
+
export declare function quotaExhaustedMessage(input: {
|
|
67
|
+
defaultMessage: string;
|
|
68
|
+
limitReset?: string;
|
|
69
|
+
retryAfter?: number;
|
|
70
|
+
serverError?: string;
|
|
71
|
+
tip: string;
|
|
72
|
+
}): string;
|
package/dist/lib/messages.js
CHANGED
|
@@ -10,6 +10,7 @@ export const PROMPTS = {
|
|
|
10
10
|
DOMAIN: 'What domain will you send from?',
|
|
11
11
|
ENTER_AFTER_RECORDS: "Press Enter once you've added the records, or 'skip'.",
|
|
12
12
|
FROM_NAME: 'Display name (optional, shown as sender name):',
|
|
13
|
+
PAUSE_CONFIRM: 'Pause this sequence? All scheduled and triggered sends will stop until you resume.',
|
|
13
14
|
REPLY_TO: 'Reply-to address (optional, press Enter to use sender email):',
|
|
14
15
|
SENDER_EMAIL: 'Sender email address:',
|
|
15
16
|
};
|
|
@@ -20,6 +21,10 @@ export const ERRORS = {
|
|
|
20
21
|
INVALID_API_KEY: `Invalid API key. Run ${chalk.cyan('mailmodo login')} to re-authenticate.`,
|
|
21
22
|
NOT_LOGGED_IN: `Not logged in. Run ${chalk.cyan('mailmodo login')} to authenticate.`,
|
|
22
23
|
NO_YAML: `No mailmodo.yaml found. Run ${chalk.cyan('mailmodo init')} first.`,
|
|
24
|
+
QUOTA_EDIT_DEFAULT: 'Edit limit reached for this month.',
|
|
25
|
+
QUOTA_EDIT_TIP: 'AI edits are limited to 50 per account per month. Manually edit the template file under ./mailmodo to make further changes.',
|
|
26
|
+
QUOTA_INIT_DEFAULT: 'Regeneration limit reached.',
|
|
27
|
+
QUOTA_INIT_TIP: `Regenerations are limited to 5 per account per month. Run ${chalk.cyan('mailmodo edit <id>')} to modify individual emails instead.`,
|
|
23
28
|
RATE_LIMIT: 'Rate limit exceeded. Please try again later.',
|
|
24
29
|
UNEXPECTED_API: 'An unexpected API error occurred.',
|
|
25
30
|
};
|
|
@@ -33,8 +38,21 @@ export const INFO = {
|
|
|
33
38
|
DOMAIN_NOT_DEPLOYED_HINT: `When ready, run: ${chalk.cyan('mailmodo domain')}\n Then: ${chalk.cyan('mailmodo deploy')}`,
|
|
34
39
|
DOMAIN_PENDING_VERIFICATION: `Your domain is not verified yet. Please verify it first. Run ${chalk.cyan('mailmodo domain --verify')} to check the status.`,
|
|
35
40
|
FREE_TIER_CAP_BLOCKED: `Monthly cap is a paid-tier setting and is not available on the free tier. Run ${chalk.cyan("'mailmodo billing --checkout'")} to add a payment method, then set a cap.`,
|
|
41
|
+
PAUSE_CANCELLED: 'Pause cancelled. Sequence is still live.',
|
|
36
42
|
SEQUENCES_NOT_DEPLOYED: `Sequences saved but ${chalk.yellow('NOT deployed')}.`,
|
|
37
43
|
};
|
|
44
|
+
export function pauseSuccess(sequenceId) {
|
|
45
|
+
return `Sequence ${chalk.cyan(sequenceId)} paused. Run ${chalk.cyan(`mailmodo deploy --resume ${sequenceId}`)} to resume.`;
|
|
46
|
+
}
|
|
47
|
+
export function pauseAlready(sequenceId) {
|
|
48
|
+
return `Sequence ${chalk.cyan(sequenceId)} is already paused. No change made.`;
|
|
49
|
+
}
|
|
50
|
+
export function resumeSuccess(sequenceId) {
|
|
51
|
+
return `Sequence ${chalk.cyan(sequenceId)} resumed. Scheduled and triggered sends are live again.`;
|
|
52
|
+
}
|
|
53
|
+
export function resumeAlready(sequenceId) {
|
|
54
|
+
return `Sequence ${chalk.cyan(sequenceId)} is already active. No change made.`;
|
|
55
|
+
}
|
|
38
56
|
export function yamlParseError(detail) {
|
|
39
57
|
return `mailmodo.yaml has invalid YAML syntax:\n${detail}`;
|
|
40
58
|
}
|
|
@@ -42,3 +60,56 @@ export function recordLabel(index) {
|
|
|
42
60
|
const labels = ['DKIM', 'DMARC', 'Return Path'];
|
|
43
61
|
return labels[index] || `Record ${index + 1}`;
|
|
44
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Parses an ISO-8601 timestamp into a `YYYY-MM-DD` date string.
|
|
65
|
+
* Returns the raw input if parsing fails so the original value is preserved.
|
|
66
|
+
*/
|
|
67
|
+
export function formatQuotaResetDate(value) {
|
|
68
|
+
if (!value)
|
|
69
|
+
return undefined;
|
|
70
|
+
const parsed = new Date(value);
|
|
71
|
+
if (Number.isNaN(parsed.getTime()))
|
|
72
|
+
return value;
|
|
73
|
+
const year = parsed.getUTCFullYear();
|
|
74
|
+
const month = String(parsed.getUTCMonth() + 1).padStart(2, '0');
|
|
75
|
+
const day = String(parsed.getUTCDate()).padStart(2, '0');
|
|
76
|
+
return `${year}-${month}-${day}`;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Formats a `retry-after` value (seconds) into a short, human-readable hint
|
|
80
|
+
* suitable for appending to a 429 error message.
|
|
81
|
+
*/
|
|
82
|
+
export function formatRetryAfter(seconds) {
|
|
83
|
+
if (typeof seconds !== 'number' || !Number.isFinite(seconds) || seconds <= 0)
|
|
84
|
+
return undefined;
|
|
85
|
+
if (seconds < 60)
|
|
86
|
+
return `${Math.round(seconds)} seconds`;
|
|
87
|
+
const minutes = Math.round(seconds / 60);
|
|
88
|
+
if (minutes < 60)
|
|
89
|
+
return `${minutes} minutes`;
|
|
90
|
+
const hours = Math.round(seconds / 3600);
|
|
91
|
+
if (hours < 24)
|
|
92
|
+
return `${hours} hours`;
|
|
93
|
+
const days = Math.round(seconds / 86_400);
|
|
94
|
+
return `${days} days`;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Builds the multi-line message printed when an AI quota (init regeneration or
|
|
98
|
+
* edit) is exhausted. `serverError` is the message returned by the API; the
|
|
99
|
+
* default text is used when the server omits it. The tip lists the next action
|
|
100
|
+
* the user can take (e.g. switch from init to edit).
|
|
101
|
+
*/
|
|
102
|
+
export function quotaExhaustedMessage(input) {
|
|
103
|
+
const headline = input.serverError || input.defaultMessage;
|
|
104
|
+
const lines = [headline];
|
|
105
|
+
const resetDate = formatQuotaResetDate(input.limitReset);
|
|
106
|
+
if (resetDate && !headline.includes(resetDate)) {
|
|
107
|
+
lines.push(`Resets on ${resetDate}.`);
|
|
108
|
+
}
|
|
109
|
+
const retryHint = formatRetryAfter(input.retryAfter);
|
|
110
|
+
if (retryHint) {
|
|
111
|
+
lines.push(`Try again in ${retryHint}.`);
|
|
112
|
+
}
|
|
113
|
+
lines.push('', input.tip);
|
|
114
|
+
return lines.join('\n');
|
|
115
|
+
}
|
package/oclif.manifest.json
CHANGED
|
@@ -140,10 +140,12 @@
|
|
|
140
140
|
"deploy": {
|
|
141
141
|
"aliases": [],
|
|
142
142
|
"args": {},
|
|
143
|
-
"description": "Deploy
|
|
143
|
+
"description": "Deploy, pause, or resume an email sequence",
|
|
144
144
|
"examples": [
|
|
145
145
|
"<%= config.bin %> deploy",
|
|
146
|
-
"<%= config.bin %> deploy --yes"
|
|
146
|
+
"<%= config.bin %> deploy --yes",
|
|
147
|
+
"<%= config.bin %> deploy --pause seq_abc123",
|
|
148
|
+
"<%= config.bin %> deploy --resume seq_abc123 --json"
|
|
147
149
|
],
|
|
148
150
|
"flags": {
|
|
149
151
|
"json": {
|
|
@@ -158,6 +160,26 @@
|
|
|
158
160
|
"name": "yes",
|
|
159
161
|
"allowNo": false,
|
|
160
162
|
"type": "boolean"
|
|
163
|
+
},
|
|
164
|
+
"pause": {
|
|
165
|
+
"description": "Pause a deployed sequence by ID (stops scheduled + triggered sends)",
|
|
166
|
+
"exclusive": [
|
|
167
|
+
"resume"
|
|
168
|
+
],
|
|
169
|
+
"name": "pause",
|
|
170
|
+
"hasDynamicHelp": false,
|
|
171
|
+
"multiple": false,
|
|
172
|
+
"type": "option"
|
|
173
|
+
},
|
|
174
|
+
"resume": {
|
|
175
|
+
"description": "Resume a paused sequence by ID",
|
|
176
|
+
"exclusive": [
|
|
177
|
+
"pause"
|
|
178
|
+
],
|
|
179
|
+
"name": "resume",
|
|
180
|
+
"hasDynamicHelp": false,
|
|
181
|
+
"multiple": false,
|
|
182
|
+
"type": "option"
|
|
161
183
|
}
|
|
162
184
|
},
|
|
163
185
|
"hasDynamicHelp": false,
|
|
@@ -176,6 +198,45 @@
|
|
|
176
198
|
"index.js"
|
|
177
199
|
]
|
|
178
200
|
},
|
|
201
|
+
"deployments": {
|
|
202
|
+
"aliases": [],
|
|
203
|
+
"args": {},
|
|
204
|
+
"description": "List every deployed sequence on this account, with the IDs needed for deploy --pause / --resume",
|
|
205
|
+
"examples": [
|
|
206
|
+
"<%= config.bin %> deployments",
|
|
207
|
+
"<%= config.bin %> deployments --json"
|
|
208
|
+
],
|
|
209
|
+
"flags": {
|
|
210
|
+
"json": {
|
|
211
|
+
"description": "Output as JSON",
|
|
212
|
+
"name": "json",
|
|
213
|
+
"allowNo": false,
|
|
214
|
+
"type": "boolean"
|
|
215
|
+
},
|
|
216
|
+
"yes": {
|
|
217
|
+
"char": "y",
|
|
218
|
+
"description": "Skip confirmation prompts",
|
|
219
|
+
"name": "yes",
|
|
220
|
+
"allowNo": false,
|
|
221
|
+
"type": "boolean"
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
"hasDynamicHelp": false,
|
|
225
|
+
"hiddenAliases": [],
|
|
226
|
+
"id": "deployments",
|
|
227
|
+
"pluginAlias": "@mailmodo/cli",
|
|
228
|
+
"pluginName": "@mailmodo/cli",
|
|
229
|
+
"pluginType": "core",
|
|
230
|
+
"strict": true,
|
|
231
|
+
"enableJsonFlag": false,
|
|
232
|
+
"isESM": true,
|
|
233
|
+
"relativePath": [
|
|
234
|
+
"dist",
|
|
235
|
+
"commands",
|
|
236
|
+
"deployments",
|
|
237
|
+
"index.js"
|
|
238
|
+
]
|
|
239
|
+
},
|
|
179
240
|
"domain": {
|
|
180
241
|
"aliases": [],
|
|
181
242
|
"args": {},
|
|
@@ -365,12 +426,13 @@
|
|
|
365
426
|
"index.js"
|
|
366
427
|
]
|
|
367
428
|
},
|
|
368
|
-
"
|
|
429
|
+
"login": {
|
|
369
430
|
"aliases": [],
|
|
370
431
|
"args": {},
|
|
371
|
-
"description": "
|
|
432
|
+
"description": "Authenticate with Mailmodo using your API key",
|
|
372
433
|
"examples": [
|
|
373
|
-
"<%= config.bin %>
|
|
434
|
+
"<%= config.bin %> login",
|
|
435
|
+
"MAILMODO_API_KEY=YOUR_API_KEY <%= config.bin %> login"
|
|
374
436
|
],
|
|
375
437
|
"flags": {
|
|
376
438
|
"json": {
|
|
@@ -389,7 +451,7 @@
|
|
|
389
451
|
},
|
|
390
452
|
"hasDynamicHelp": false,
|
|
391
453
|
"hiddenAliases": [],
|
|
392
|
-
"id": "
|
|
454
|
+
"id": "login",
|
|
393
455
|
"pluginAlias": "@mailmodo/cli",
|
|
394
456
|
"pluginName": "@mailmodo/cli",
|
|
395
457
|
"pluginType": "core",
|
|
@@ -399,17 +461,16 @@
|
|
|
399
461
|
"relativePath": [
|
|
400
462
|
"dist",
|
|
401
463
|
"commands",
|
|
402
|
-
"
|
|
464
|
+
"login",
|
|
403
465
|
"index.js"
|
|
404
466
|
]
|
|
405
467
|
},
|
|
406
|
-
"
|
|
468
|
+
"logout": {
|
|
407
469
|
"aliases": [],
|
|
408
470
|
"args": {},
|
|
409
|
-
"description": "
|
|
471
|
+
"description": "Sign out by removing saved credentials from this machine",
|
|
410
472
|
"examples": [
|
|
411
|
-
"<%= config.bin %>
|
|
412
|
-
"MAILMODO_API_KEY=YOUR_API_KEY <%= config.bin %> login"
|
|
473
|
+
"<%= config.bin %> logout"
|
|
413
474
|
],
|
|
414
475
|
"flags": {
|
|
415
476
|
"json": {
|
|
@@ -428,7 +489,7 @@
|
|
|
428
489
|
},
|
|
429
490
|
"hasDynamicHelp": false,
|
|
430
491
|
"hiddenAliases": [],
|
|
431
|
-
"id": "
|
|
492
|
+
"id": "logout",
|
|
432
493
|
"pluginAlias": "@mailmodo/cli",
|
|
433
494
|
"pluginName": "@mailmodo/cli",
|
|
434
495
|
"pluginType": "core",
|
|
@@ -438,23 +499,19 @@
|
|
|
438
499
|
"relativePath": [
|
|
439
500
|
"dist",
|
|
440
501
|
"commands",
|
|
441
|
-
"
|
|
502
|
+
"logout",
|
|
442
503
|
"index.js"
|
|
443
504
|
]
|
|
444
505
|
},
|
|
445
|
-
"
|
|
506
|
+
"logs": {
|
|
446
507
|
"aliases": [],
|
|
447
|
-
"args": {
|
|
448
|
-
|
|
449
|
-
"description": "Email template ID to preview",
|
|
450
|
-
"name": "id"
|
|
451
|
-
}
|
|
452
|
-
},
|
|
453
|
-
"description": "Preview an email in browser, as text, or send a test",
|
|
508
|
+
"args": {},
|
|
509
|
+
"description": "View email send logs and delivery events",
|
|
454
510
|
"examples": [
|
|
455
|
-
"<%= config.bin %>
|
|
456
|
-
"<%= config.bin %>
|
|
457
|
-
"<%= config.bin %>
|
|
511
|
+
"<%= config.bin %> logs",
|
|
512
|
+
"<%= config.bin %> logs --email sarah@example.com",
|
|
513
|
+
"<%= config.bin %> logs --failed",
|
|
514
|
+
"<%= config.bin %> logs --json"
|
|
458
515
|
],
|
|
459
516
|
"flags": {
|
|
460
517
|
"json": {
|
|
@@ -470,23 +527,39 @@
|
|
|
470
527
|
"allowNo": false,
|
|
471
528
|
"type": "boolean"
|
|
472
529
|
},
|
|
473
|
-
"
|
|
474
|
-
"description": "
|
|
475
|
-
"name": "
|
|
530
|
+
"email": {
|
|
531
|
+
"description": "Filter logs by contact email",
|
|
532
|
+
"name": "email",
|
|
476
533
|
"hasDynamicHelp": false,
|
|
477
534
|
"multiple": false,
|
|
478
535
|
"type": "option"
|
|
479
536
|
},
|
|
480
|
-
"
|
|
481
|
-
"description": "
|
|
482
|
-
"name": "
|
|
537
|
+
"failed": {
|
|
538
|
+
"description": "Show only failed/bounced events",
|
|
539
|
+
"name": "failed",
|
|
483
540
|
"allowNo": false,
|
|
484
541
|
"type": "boolean"
|
|
542
|
+
},
|
|
543
|
+
"limit": {
|
|
544
|
+
"description": "Entries per page (max 200)",
|
|
545
|
+
"name": "limit",
|
|
546
|
+
"default": 50,
|
|
547
|
+
"hasDynamicHelp": false,
|
|
548
|
+
"multiple": false,
|
|
549
|
+
"type": "option"
|
|
550
|
+
},
|
|
551
|
+
"page": {
|
|
552
|
+
"description": "Page number",
|
|
553
|
+
"name": "page",
|
|
554
|
+
"default": 1,
|
|
555
|
+
"hasDynamicHelp": false,
|
|
556
|
+
"multiple": false,
|
|
557
|
+
"type": "option"
|
|
485
558
|
}
|
|
486
559
|
},
|
|
487
560
|
"hasDynamicHelp": false,
|
|
488
561
|
"hiddenAliases": [],
|
|
489
|
-
"id": "
|
|
562
|
+
"id": "logs",
|
|
490
563
|
"pluginAlias": "@mailmodo/cli",
|
|
491
564
|
"pluginName": "@mailmodo/cli",
|
|
492
565
|
"pluginType": "core",
|
|
@@ -496,19 +569,23 @@
|
|
|
496
569
|
"relativePath": [
|
|
497
570
|
"dist",
|
|
498
571
|
"commands",
|
|
499
|
-
"
|
|
572
|
+
"logs",
|
|
500
573
|
"index.js"
|
|
501
574
|
]
|
|
502
575
|
},
|
|
503
|
-
"
|
|
576
|
+
"preview": {
|
|
504
577
|
"aliases": [],
|
|
505
|
-
"args": {
|
|
506
|
-
|
|
578
|
+
"args": {
|
|
579
|
+
"id": {
|
|
580
|
+
"description": "Email template ID to preview",
|
|
581
|
+
"name": "id"
|
|
582
|
+
}
|
|
583
|
+
},
|
|
584
|
+
"description": "Preview an email in browser, as text, or send a test",
|
|
507
585
|
"examples": [
|
|
508
|
-
"<%= config.bin %>
|
|
509
|
-
"<%= config.bin %>
|
|
510
|
-
"<%= config.bin %>
|
|
511
|
-
"<%= config.bin %> logs --json"
|
|
586
|
+
"<%= config.bin %> preview welcome",
|
|
587
|
+
"<%= config.bin %> preview welcome --text",
|
|
588
|
+
"<%= config.bin %> preview welcome --send me@example.com"
|
|
512
589
|
],
|
|
513
590
|
"flags": {
|
|
514
591
|
"json": {
|
|
@@ -524,39 +601,23 @@
|
|
|
524
601
|
"allowNo": false,
|
|
525
602
|
"type": "boolean"
|
|
526
603
|
},
|
|
527
|
-
"
|
|
528
|
-
"description": "
|
|
529
|
-
"name": "
|
|
604
|
+
"send": {
|
|
605
|
+
"description": "Send test email to this address",
|
|
606
|
+
"name": "send",
|
|
530
607
|
"hasDynamicHelp": false,
|
|
531
608
|
"multiple": false,
|
|
532
609
|
"type": "option"
|
|
533
610
|
},
|
|
534
|
-
"
|
|
535
|
-
"description": "
|
|
536
|
-
"name": "
|
|
611
|
+
"text": {
|
|
612
|
+
"description": "Output plain text version (for AI agents)",
|
|
613
|
+
"name": "text",
|
|
537
614
|
"allowNo": false,
|
|
538
615
|
"type": "boolean"
|
|
539
|
-
},
|
|
540
|
-
"limit": {
|
|
541
|
-
"description": "Entries per page (max 200)",
|
|
542
|
-
"name": "limit",
|
|
543
|
-
"default": 50,
|
|
544
|
-
"hasDynamicHelp": false,
|
|
545
|
-
"multiple": false,
|
|
546
|
-
"type": "option"
|
|
547
|
-
},
|
|
548
|
-
"page": {
|
|
549
|
-
"description": "Page number",
|
|
550
|
-
"name": "page",
|
|
551
|
-
"default": 1,
|
|
552
|
-
"hasDynamicHelp": false,
|
|
553
|
-
"multiple": false,
|
|
554
|
-
"type": "option"
|
|
555
616
|
}
|
|
556
617
|
},
|
|
557
618
|
"hasDynamicHelp": false,
|
|
558
619
|
"hiddenAliases": [],
|
|
559
|
-
"id": "
|
|
620
|
+
"id": "preview",
|
|
560
621
|
"pluginAlias": "@mailmodo/cli",
|
|
561
622
|
"pluginName": "@mailmodo/cli",
|
|
562
623
|
"pluginType": "core",
|
|
@@ -566,7 +627,7 @@
|
|
|
566
627
|
"relativePath": [
|
|
567
628
|
"dist",
|
|
568
629
|
"commands",
|
|
569
|
-
"
|
|
630
|
+
"preview",
|
|
570
631
|
"index.js"
|
|
571
632
|
]
|
|
572
633
|
},
|
|
@@ -657,5 +718,5 @@
|
|
|
657
718
|
]
|
|
658
719
|
}
|
|
659
720
|
},
|
|
660
|
-
"version": "0.0.
|
|
721
|
+
"version": "0.0.50-beta.pr52.80"
|
|
661
722
|
}
|