@mailmodo/cli 0.0.29-beta.pr31.49 → 0.0.30-beta.pr32.50
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.
|
@@ -10,6 +10,17 @@ export default class Edit extends BaseCommand {
|
|
|
10
10
|
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
11
|
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
12
|
};
|
|
13
|
+
run(): Promise<void>;
|
|
14
|
+
private runEditStep;
|
|
15
|
+
private handleUserAction;
|
|
16
|
+
private callEditApi;
|
|
17
|
+
private finalizeEdit;
|
|
18
|
+
private applyEmailChanges;
|
|
19
|
+
private persistChanges;
|
|
20
|
+
private logJsonResult;
|
|
21
|
+
private handleAcceptOutput;
|
|
22
|
+
private promptEditAction;
|
|
23
|
+
private askChangeDescription;
|
|
13
24
|
private showFieldDiff;
|
|
14
25
|
private stripHtml;
|
|
15
26
|
private truncate;
|
|
@@ -20,5 +31,4 @@ export default class Edit extends BaseCommand {
|
|
|
20
31
|
private showUnchanged;
|
|
21
32
|
private buildDiffPreview;
|
|
22
33
|
private showChangeSummary;
|
|
23
|
-
run(): Promise<void>;
|
|
24
34
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Args, Flags } from '@oclif/core';
|
|
2
|
-
import { confirm, input } from '@inquirer/prompts';
|
|
2
|
+
import { confirm, input, select } from '@inquirer/prompts';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { BaseCommand } from '../../lib/base-command.js';
|
|
5
5
|
import { API_ENDPOINTS } from '../../lib/constants.js';
|
|
@@ -22,6 +22,149 @@ export default class Edit extends BaseCommand {
|
|
|
22
22
|
description: 'Natural language description of the change',
|
|
23
23
|
}),
|
|
24
24
|
};
|
|
25
|
+
async run() {
|
|
26
|
+
const { args, flags } = await this.parse(Edit);
|
|
27
|
+
await this.ensureAuth();
|
|
28
|
+
const yamlConfig = await this.ensureYaml();
|
|
29
|
+
const emailIndex = yamlConfig.emails.findIndex((e) => e.id === args.id);
|
|
30
|
+
if (emailIndex === -1) {
|
|
31
|
+
this.error(`Email '${args.id}' not found in mailmodo.yaml.`);
|
|
32
|
+
}
|
|
33
|
+
const ctx = {
|
|
34
|
+
email: yamlConfig.emails[emailIndex],
|
|
35
|
+
emailIndex,
|
|
36
|
+
templateHtml: await loadTemplate(`${yamlConfig.emails[emailIndex].id}.html`),
|
|
37
|
+
yamlConfig,
|
|
38
|
+
};
|
|
39
|
+
const editFlags = {
|
|
40
|
+
json: flags.json ?? false,
|
|
41
|
+
yes: flags.yes ?? false,
|
|
42
|
+
};
|
|
43
|
+
const initialChange = flags.change ?? (await this.askChangeDescription());
|
|
44
|
+
await this.runEditStep(ctx, initialChange, editFlags);
|
|
45
|
+
}
|
|
46
|
+
async runEditStep(ctx, changeDescription, flags) {
|
|
47
|
+
const response = await this.withApiSpinner({ json: flags.json, text: ' Applying AI edits...' }, () => this.callEditApi(changeDescription, ctx.email, ctx.templateHtml));
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
this.handleApiError(response);
|
|
50
|
+
}
|
|
51
|
+
const updated = response.data;
|
|
52
|
+
if (flags.json) {
|
|
53
|
+
this.log(JSON.stringify(this.buildDiffPreview(ctx.email, updated, ctx.templateHtml), null, 2));
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
this.showChangeSummary(ctx.email, updated, ctx.templateHtml);
|
|
57
|
+
}
|
|
58
|
+
if (flags.yes) {
|
|
59
|
+
await this.finalizeEdit(ctx, updated, flags);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
await this.handleUserAction(ctx, updated, changeDescription, flags);
|
|
63
|
+
}
|
|
64
|
+
async handleUserAction(ctx, updated, changeDescription, flags) {
|
|
65
|
+
this.log('');
|
|
66
|
+
const action = await this.promptEditAction();
|
|
67
|
+
if (action === 'skip') {
|
|
68
|
+
this.log('\n Changes discarded.\n');
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (action === 'retry') {
|
|
72
|
+
const newChange = await this.askChangeDescription();
|
|
73
|
+
await this.runEditStep(ctx, newChange, flags);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
await this.finalizeEdit(ctx, updated, flags);
|
|
77
|
+
}
|
|
78
|
+
callEditApi(changeDescription, email, templateHtml) {
|
|
79
|
+
return this.apiClient.post(API_ENDPOINTS.EDIT, {
|
|
80
|
+
changeRequest: changeDescription,
|
|
81
|
+
currentEmail: {
|
|
82
|
+
condition: email.condition,
|
|
83
|
+
goal: email.goal,
|
|
84
|
+
html: templateHtml,
|
|
85
|
+
id: email.id,
|
|
86
|
+
previewText: email.previewText,
|
|
87
|
+
subject: email.subject,
|
|
88
|
+
trigger: email.trigger,
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
async finalizeEdit(ctx, updated, flags) {
|
|
93
|
+
const oldSubject = ctx.email.subject;
|
|
94
|
+
this.applyEmailChanges(ctx.email, updated);
|
|
95
|
+
await this.persistChanges(ctx, updated);
|
|
96
|
+
if (flags.json) {
|
|
97
|
+
this.logJsonResult(ctx.email, updated, oldSubject);
|
|
98
|
+
}
|
|
99
|
+
else if (flags.yes) {
|
|
100
|
+
this.log(`\n Updated ${chalk.green('mailmodo.yaml')}\n`);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
await this.handleAcceptOutput(ctx.email);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
applyEmailChanges(email, updated) {
|
|
107
|
+
if (updated.subject)
|
|
108
|
+
email.subject = updated.subject;
|
|
109
|
+
if (updated.previewText)
|
|
110
|
+
email.previewText = updated.previewText;
|
|
111
|
+
if (updated.ctaText)
|
|
112
|
+
email.ctaText = updated.ctaText;
|
|
113
|
+
}
|
|
114
|
+
async persistChanges(ctx, updated) {
|
|
115
|
+
const updatedYaml = {
|
|
116
|
+
...ctx.yamlConfig,
|
|
117
|
+
emails: [...ctx.yamlConfig.emails],
|
|
118
|
+
};
|
|
119
|
+
updatedYaml.emails[ctx.emailIndex] = ctx.email;
|
|
120
|
+
await saveYaml(updatedYaml);
|
|
121
|
+
if (updated.html) {
|
|
122
|
+
await saveTemplate(`${ctx.email.id}.html`, updated.html);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
logJsonResult(email, updated, oldSubject) {
|
|
126
|
+
this.log(JSON.stringify({
|
|
127
|
+
diff: {
|
|
128
|
+
previewText: updated.previewText && updated.previewText !== email.previewText
|
|
129
|
+
? { new: updated.previewText, old: email.previewText }
|
|
130
|
+
: undefined,
|
|
131
|
+
subject: oldSubject === email.subject
|
|
132
|
+
? undefined
|
|
133
|
+
: { new: email.subject, old: oldSubject },
|
|
134
|
+
},
|
|
135
|
+
email,
|
|
136
|
+
status: 'updated',
|
|
137
|
+
}, null, 2));
|
|
138
|
+
}
|
|
139
|
+
async handleAcceptOutput(email) {
|
|
140
|
+
this.log(`\n Updated ${chalk.green('mailmodo.yaml')}`);
|
|
141
|
+
const shouldPreview = await confirm({
|
|
142
|
+
default: true,
|
|
143
|
+
message: 'Preview the change?',
|
|
144
|
+
});
|
|
145
|
+
if (shouldPreview) {
|
|
146
|
+
await this.config.runCommand('preview', [email.id]);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
this.log(` Run: ${chalk.cyan(`mailmodo preview ${email.id}`)}\n`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async promptEditAction() {
|
|
153
|
+
return select({
|
|
154
|
+
message: 'Accept, try again, or skip?',
|
|
155
|
+
choices: [
|
|
156
|
+
{ name: 'Accept', value: 'accept' },
|
|
157
|
+
{ name: 'Try again', value: 'retry' },
|
|
158
|
+
{ name: 'Skip', value: 'skip' },
|
|
159
|
+
],
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
async askChangeDescription() {
|
|
163
|
+
return input({
|
|
164
|
+
message: 'What do you want to change?',
|
|
165
|
+
validate: (value) => value?.trim() ? true : 'Please describe the change',
|
|
166
|
+
});
|
|
167
|
+
}
|
|
25
168
|
showFieldDiff(label, oldVal, newVal) {
|
|
26
169
|
if (!newVal || oldVal === newVal)
|
|
27
170
|
return false;
|
|
@@ -139,88 +282,4 @@ export default class Edit extends BaseCommand {
|
|
|
139
282
|
this.showSuggestedChanges(email, updated, templateHtml, changed);
|
|
140
283
|
this.showUnchanged(email, templateHtml, changed);
|
|
141
284
|
}
|
|
142
|
-
async run() {
|
|
143
|
-
const { args, flags } = await this.parse(Edit);
|
|
144
|
-
await this.ensureAuth();
|
|
145
|
-
const yamlConfig = await this.ensureYaml();
|
|
146
|
-
const emailIndex = yamlConfig.emails.findIndex((e) => e.id === args.id);
|
|
147
|
-
if (emailIndex === -1) {
|
|
148
|
-
this.error(`Email '${args.id}' not found in mailmodo.yaml.`);
|
|
149
|
-
}
|
|
150
|
-
const email = yamlConfig.emails[emailIndex];
|
|
151
|
-
const templateHtml = await loadTemplate(`${email.id}.html`);
|
|
152
|
-
let changeDescription = flags.change;
|
|
153
|
-
if (!changeDescription) {
|
|
154
|
-
changeDescription = await input({
|
|
155
|
-
message: 'What do you want to change?',
|
|
156
|
-
validate: (value) => value?.trim() ? true : 'Please describe the change',
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
const response = await this.withApiSpinner({ json: flags.json, text: ' Applying AI edits...' }, () => this.apiClient.post(API_ENDPOINTS.EDIT, {
|
|
160
|
-
changeRequest: changeDescription,
|
|
161
|
-
currentEmail: {
|
|
162
|
-
condition: email.condition,
|
|
163
|
-
goal: email.goal,
|
|
164
|
-
html: templateHtml,
|
|
165
|
-
id: email.id,
|
|
166
|
-
previewText: email.previewText,
|
|
167
|
-
subject: email.subject,
|
|
168
|
-
trigger: email.trigger,
|
|
169
|
-
},
|
|
170
|
-
}));
|
|
171
|
-
if (!response.ok) {
|
|
172
|
-
this.handleApiError(response);
|
|
173
|
-
}
|
|
174
|
-
const updated = response.data;
|
|
175
|
-
if (flags.json) {
|
|
176
|
-
this.log(JSON.stringify(this.buildDiffPreview(email, updated, templateHtml), null, 2));
|
|
177
|
-
}
|
|
178
|
-
else {
|
|
179
|
-
this.showChangeSummary(email, updated, templateHtml);
|
|
180
|
-
}
|
|
181
|
-
if (!flags.yes) {
|
|
182
|
-
const accepted = await confirm({
|
|
183
|
-
default: true,
|
|
184
|
-
message: 'Accept changes?',
|
|
185
|
-
});
|
|
186
|
-
if (!accepted) {
|
|
187
|
-
this.log('\n Changes discarded.\n');
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
const oldSubject = email.subject;
|
|
192
|
-
const newSubject = updated.subject || email.subject;
|
|
193
|
-
if (updated.subject)
|
|
194
|
-
email.subject = updated.subject;
|
|
195
|
-
if (updated.previewText)
|
|
196
|
-
email.previewText = updated.previewText;
|
|
197
|
-
if (updated.ctaText)
|
|
198
|
-
email.ctaText = updated.ctaText;
|
|
199
|
-
const updatedYaml = {
|
|
200
|
-
...yamlConfig,
|
|
201
|
-
emails: [...yamlConfig.emails],
|
|
202
|
-
};
|
|
203
|
-
updatedYaml.emails[emailIndex] = email;
|
|
204
|
-
await saveYaml(updatedYaml);
|
|
205
|
-
if (updated.html) {
|
|
206
|
-
await saveTemplate(`${email.id}.html`, updated.html);
|
|
207
|
-
}
|
|
208
|
-
if (flags.json) {
|
|
209
|
-
this.log(JSON.stringify({
|
|
210
|
-
diff: {
|
|
211
|
-
previewText: updated.previewText && updated.previewText !== email.previewText
|
|
212
|
-
? { new: updated.previewText, old: email.previewText }
|
|
213
|
-
: undefined,
|
|
214
|
-
subject: oldSubject === newSubject
|
|
215
|
-
? undefined
|
|
216
|
-
: { new: newSubject, old: oldSubject },
|
|
217
|
-
},
|
|
218
|
-
email,
|
|
219
|
-
status: 'updated',
|
|
220
|
-
}, null, 2));
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
this.log(`\n Updated ${chalk.green('mailmodo.yaml')}`);
|
|
224
|
-
this.log(` Preview the change: ${chalk.cyan(`mailmodo preview ${email.id}`)}\n`);
|
|
225
|
-
}
|
|
226
285
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { confirm, input } from '@inquirer/prompts';
|
|
2
4
|
import chalk from 'chalk';
|
|
5
|
+
import open from 'open';
|
|
3
6
|
import { BaseCommand } from '../../lib/base-command.js';
|
|
4
7
|
export default class Emails extends BaseCommand {
|
|
5
8
|
static description = 'List and view configured email sequences';
|
|
@@ -61,7 +64,48 @@ export default class Emails extends BaseCommand {
|
|
|
61
64
|
this.log(` ${chalk.bold('Goal:')} ${email.goal}`);
|
|
62
65
|
}
|
|
63
66
|
this.log('');
|
|
67
|
+
const openIt = await confirm({
|
|
68
|
+
default: true,
|
|
69
|
+
message: 'Open template in editor?',
|
|
70
|
+
});
|
|
71
|
+
if (openIt) {
|
|
72
|
+
await this.openTemplateInEditor(email.template);
|
|
73
|
+
}
|
|
64
74
|
}
|
|
65
75
|
}
|
|
66
76
|
}
|
|
77
|
+
async openTemplateInEditor(template) {
|
|
78
|
+
const templatePath = join(process.cwd(), template);
|
|
79
|
+
this.log(`\n Opening ${template}...\n`);
|
|
80
|
+
const editor = process.env.VISUAL || process.env.EDITOR;
|
|
81
|
+
if (editor) {
|
|
82
|
+
const child = spawn(editor, [templatePath], { stdio: 'inherit' });
|
|
83
|
+
await new Promise((resolve) => {
|
|
84
|
+
child.on('close', resolve);
|
|
85
|
+
});
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (await this.trySpawnEditor('code', templatePath))
|
|
89
|
+
return;
|
|
90
|
+
try {
|
|
91
|
+
await open(templatePath);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
this.log(` ${chalk.dim(`Could not open editor. Open the file manually: ${templatePath}`)}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
trySpawnEditor(editor, filePath) {
|
|
98
|
+
const [cmd, args] = process.platform === 'win32'
|
|
99
|
+
? ['cmd.exe', ['/c', editor, filePath]]
|
|
100
|
+
: [editor, [filePath]];
|
|
101
|
+
return new Promise((resolve) => {
|
|
102
|
+
const child = spawn(cmd, [...args], { stdio: 'ignore' });
|
|
103
|
+
child.on('error', () => {
|
|
104
|
+
resolve(false);
|
|
105
|
+
});
|
|
106
|
+
child.on('close', (code) => {
|
|
107
|
+
resolve(code === 0);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
67
111
|
}
|
package/oclif.manifest.json
CHANGED
|
@@ -228,19 +228,13 @@
|
|
|
228
228
|
"index.js"
|
|
229
229
|
]
|
|
230
230
|
},
|
|
231
|
-
"
|
|
231
|
+
"emails": {
|
|
232
232
|
"aliases": [],
|
|
233
|
-
"args": {
|
|
234
|
-
|
|
235
|
-
"description": "Email template ID to edit",
|
|
236
|
-
"name": "id",
|
|
237
|
-
"required": true
|
|
238
|
-
}
|
|
239
|
-
},
|
|
240
|
-
"description": "Edit an email using AI-assisted natural language changes",
|
|
233
|
+
"args": {},
|
|
234
|
+
"description": "List and view configured email sequences",
|
|
241
235
|
"examples": [
|
|
242
|
-
"<%= config.bin %>
|
|
243
|
-
"<%= config.bin %>
|
|
236
|
+
"<%= config.bin %> emails",
|
|
237
|
+
"<%= config.bin %> emails --json"
|
|
244
238
|
],
|
|
245
239
|
"flags": {
|
|
246
240
|
"json": {
|
|
@@ -255,18 +249,11 @@
|
|
|
255
249
|
"name": "yes",
|
|
256
250
|
"allowNo": false,
|
|
257
251
|
"type": "boolean"
|
|
258
|
-
},
|
|
259
|
-
"change": {
|
|
260
|
-
"description": "Natural language description of the change",
|
|
261
|
-
"name": "change",
|
|
262
|
-
"hasDynamicHelp": false,
|
|
263
|
-
"multiple": false,
|
|
264
|
-
"type": "option"
|
|
265
252
|
}
|
|
266
253
|
},
|
|
267
254
|
"hasDynamicHelp": false,
|
|
268
255
|
"hiddenAliases": [],
|
|
269
|
-
"id": "
|
|
256
|
+
"id": "emails",
|
|
270
257
|
"pluginAlias": "@mailmodo/cli",
|
|
271
258
|
"pluginName": "@mailmodo/cli",
|
|
272
259
|
"pluginType": "core",
|
|
@@ -276,17 +263,23 @@
|
|
|
276
263
|
"relativePath": [
|
|
277
264
|
"dist",
|
|
278
265
|
"commands",
|
|
279
|
-
"
|
|
266
|
+
"emails",
|
|
280
267
|
"index.js"
|
|
281
268
|
]
|
|
282
269
|
},
|
|
283
|
-
"
|
|
270
|
+
"edit": {
|
|
284
271
|
"aliases": [],
|
|
285
|
-
"args": {
|
|
286
|
-
|
|
272
|
+
"args": {
|
|
273
|
+
"id": {
|
|
274
|
+
"description": "Email template ID to edit",
|
|
275
|
+
"name": "id",
|
|
276
|
+
"required": true
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
"description": "Edit an email using AI-assisted natural language changes",
|
|
287
280
|
"examples": [
|
|
288
|
-
"<%= config.bin %>
|
|
289
|
-
"<%= config.bin %>
|
|
281
|
+
"<%= config.bin %> edit welcome",
|
|
282
|
+
"<%= config.bin %> edit welcome --change \"make subject more urgent\" --yes"
|
|
290
283
|
],
|
|
291
284
|
"flags": {
|
|
292
285
|
"json": {
|
|
@@ -301,11 +294,18 @@
|
|
|
301
294
|
"name": "yes",
|
|
302
295
|
"allowNo": false,
|
|
303
296
|
"type": "boolean"
|
|
297
|
+
},
|
|
298
|
+
"change": {
|
|
299
|
+
"description": "Natural language description of the change",
|
|
300
|
+
"name": "change",
|
|
301
|
+
"hasDynamicHelp": false,
|
|
302
|
+
"multiple": false,
|
|
303
|
+
"type": "option"
|
|
304
304
|
}
|
|
305
305
|
},
|
|
306
306
|
"hasDynamicHelp": false,
|
|
307
307
|
"hiddenAliases": [],
|
|
308
|
-
"id": "
|
|
308
|
+
"id": "edit",
|
|
309
309
|
"pluginAlias": "@mailmodo/cli",
|
|
310
310
|
"pluginName": "@mailmodo/cli",
|
|
311
311
|
"pluginType": "core",
|
|
@@ -315,7 +315,7 @@
|
|
|
315
315
|
"relativePath": [
|
|
316
316
|
"dist",
|
|
317
317
|
"commands",
|
|
318
|
-
"
|
|
318
|
+
"edit",
|
|
319
319
|
"index.js"
|
|
320
320
|
]
|
|
321
321
|
},
|
|
@@ -657,5 +657,5 @@
|
|
|
657
657
|
]
|
|
658
658
|
}
|
|
659
659
|
},
|
|
660
|
-
"version": "0.0.
|
|
660
|
+
"version": "0.0.30-beta.pr32.50"
|
|
661
661
|
}
|
package/package.json
CHANGED