@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
  }
@@ -7,4 +7,6 @@ export default class Emails extends BaseCommand {
7
7
  yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
8
  };
9
9
  run(): Promise<void>;
10
+ private openTemplateInEditor;
11
+ private trySpawnEditor;
10
12
  }
@@ -1,5 +1,8 @@
1
- import { input } from '@inquirer/prompts';
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
  }
@@ -228,19 +228,13 @@
228
228
  "index.js"
229
229
  ]
230
230
  },
231
- "edit": {
231
+ "emails": {
232
232
  "aliases": [],
233
- "args": {
234
- "id": {
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 %> edit welcome",
243
- "<%= config.bin %> edit welcome --change \"make subject more urgent\" --yes"
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": "edit",
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
- "edit",
266
+ "emails",
280
267
  "index.js"
281
268
  ]
282
269
  },
283
- "emails": {
270
+ "edit": {
284
271
  "aliases": [],
285
- "args": {},
286
- "description": "List and view configured email sequences",
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 %> emails",
289
- "<%= config.bin %> emails --json"
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": "emails",
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
- "emails",
318
+ "edit",
319
319
  "index.js"
320
320
  ]
321
321
  },
@@ -657,5 +657,5 @@
657
657
  ]
658
658
  }
659
659
  },
660
- "version": "0.0.29-beta.pr31.49"
660
+ "version": "0.0.30-beta.pr32.50"
661
661
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mailmodo/cli",
3
3
  "description": "Email lifecycle automation for the AI-native builder generation.",
4
- "version": "0.0.29-beta.pr31.49",
4
+ "version": "0.0.30-beta.pr32.50",
5
5
  "author": "provishalk",
6
6
  "bin": {
7
7
  "mailmodo": "bin/run.js"