@mailmodo/cli 0.0.30-beta.pr32.51 → 0.0.30-beta.pr32.53

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.
@@ -3,7 +3,7 @@ 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';
6
- import { loadTemplate, resolveTemplateFilename, saveTemplate, saveYaml, } from '../../lib/yaml-config.js';
6
+ import { loadTemplate, getTemplateFilename, saveTemplate, saveYaml, } from '../../lib/yaml-config.js';
7
7
  export default class Edit extends BaseCommand {
8
8
  static args = {
9
9
  id: Args.string({
@@ -31,7 +31,7 @@ export default class Edit extends BaseCommand {
31
31
  this.error(`Email '${args.id}' not found in mailmodo.yaml.`);
32
32
  }
33
33
  const email = yamlConfig.emails[emailIndex];
34
- const templateFilename = resolveTemplateFilename(email.id, email.style, yamlConfig.project?.emailStyle);
34
+ const templateFilename = getTemplateFilename(email.id, email.style, yamlConfig.project?.emailStyle);
35
35
  const ctx = {
36
36
  email,
37
37
  emailIndex,
@@ -43,7 +43,7 @@ export default class Edit extends BaseCommand {
43
43
  json: flags.json ?? false,
44
44
  yes: flags.yes ?? false,
45
45
  };
46
- const initialChange = flags.change ?? (await this.askChangeDescription());
46
+ const initialChange = flags.change?.trim() || (await this.askChangeDescription());
47
47
  await this.runEditStep(ctx, initialChange, editFlags);
48
48
  }
49
49
  async runEditStep(ctx, changeDescription, flags) {
@@ -79,11 +79,16 @@ export default class Emails extends BaseCommand {
79
79
  this.log(`\n Opening ${template}...\n`);
80
80
  const editor = process.env.VISUAL || process.env.EDITOR;
81
81
  if (editor) {
82
- const child = spawn(editor, [templatePath], { stdio: 'inherit' });
83
- await new Promise((resolve) => {
84
- child.on('close', resolve);
82
+ const [cmd, ...editorArgs] = editor.trim().split(/\s+/);
83
+ const launched = await new Promise((resolve) => {
84
+ const child = spawn(cmd, [...editorArgs, templatePath], {
85
+ stdio: 'inherit',
86
+ });
87
+ child.on('error', () => resolve(false));
88
+ child.on('close', () => resolve(true));
85
89
  });
86
- return;
90
+ if (launched)
91
+ return;
87
92
  }
88
93
  if (await this.trySpawnEditor('code', templatePath))
89
94
  return;
@@ -23,8 +23,12 @@ export default class Preview extends BaseCommand {
23
23
  */
24
24
  private sendTestEmail;
25
25
  /**
26
- * Starts a local HTTP server on PREVIEW_PORT to serve the rendered email
27
- * template, then opens the user's default browser to view it.
26
+ * Probes ports starting at startPort and returns the first one not in use.
27
+ */
28
+ private findAvailablePort;
29
+ /**
30
+ * Starts a local HTTP server to serve the rendered email template,
31
+ * then opens the user's default browser to view it.
28
32
  */
29
33
  private startPreviewServer;
30
34
  }
@@ -4,7 +4,7 @@ import chalk from 'chalk';
4
4
  import open from 'open';
5
5
  import { BaseCommand } from '../../lib/base-command.js';
6
6
  import { API_ENDPOINTS, PREVIEW_PORT } from '../../lib/constants.js';
7
- import { loadTemplate, resolveTemplateFilename, } from '../../lib/yaml-config.js';
7
+ import { loadTemplate, getEmailStyle, getTemplateFilename, } from '../../lib/yaml-config.js';
8
8
  /* eslint-disable camelcase */
9
9
  const SAMPLE_DATA = Object.freeze({
10
10
  app_url: 'https://yourapp.com',
@@ -84,7 +84,8 @@ export default class Preview extends BaseCommand {
84
84
  app_url: yamlConfig.project?.url || 'https://yourapp.com', // eslint-disable-line camelcase
85
85
  product_name: yamlConfig.project?.name || 'YourApp', // eslint-disable-line camelcase
86
86
  };
87
- const templateHtml = await loadTemplate(resolveTemplateFilename(email.id, email.style, yamlConfig.project?.emailStyle));
87
+ const effectiveStyle = getEmailStyle(email.style, yamlConfig.project?.emailStyle);
88
+ const templateHtml = await loadTemplate(getTemplateFilename(email.id, email.style, yamlConfig.project?.emailStyle));
88
89
  if (flags.send) {
89
90
  const rendered = templateHtml
90
91
  ? renderTemplate(templateHtml, sampleData)
@@ -100,7 +101,10 @@ export default class Preview extends BaseCommand {
100
101
  await this.renderText(email, templateHtml, sampleData, flags.json);
101
102
  return;
102
103
  }
103
- await this.startPreviewServer(email, templateHtml, sampleData, flags.json);
104
+ await this.startPreviewServer(email, templateHtml, sampleData, {
105
+ effectiveStyle,
106
+ jsonOutput: flags.json,
107
+ });
104
108
  }
105
109
  /**
106
110
  * Renders a plain text version of the email to stdout. Used by AI agents
@@ -154,10 +158,28 @@ export default class Preview extends BaseCommand {
154
158
  this.log('');
155
159
  }
156
160
  /**
157
- * Starts a local HTTP server on PREVIEW_PORT to serve the rendered email
158
- * template, then opens the user's default browser to view it.
161
+ * Probes ports starting at startPort and returns the first one not in use.
159
162
  */
160
- async startPreviewServer(email, templateHtml, sampleData, jsonOutput) {
163
+ async findAvailablePort(startPort, endPort = startPort + 10) {
164
+ if (startPort > endPort) {
165
+ throw new Error(`No available port found starting from port ${endPort - 10}`);
166
+ }
167
+ const available = await new Promise((resolve) => {
168
+ const probe = createServer();
169
+ probe.once('error', () => resolve(false));
170
+ probe.once('listening', () => probe.close(() => resolve(true)));
171
+ probe.listen(startPort);
172
+ });
173
+ return available
174
+ ? startPort
175
+ : this.findAvailablePort(startPort + 1, endPort);
176
+ }
177
+ /**
178
+ * Starts a local HTTP server to serve the rendered email template,
179
+ * then opens the user's default browser to view it.
180
+ */
181
+ async startPreviewServer(email, templateHtml, sampleData, opts) {
182
+ const { effectiveStyle, jsonOutput } = opts;
161
183
  const rendered = templateHtml
162
184
  ? renderTemplate(templateHtml, sampleData)
163
185
  : '<p>No template found.</p>';
@@ -183,7 +205,7 @@ export default class Preview extends BaseCommand {
183
205
  <body>
184
206
  <div class="preview-bar">
185
207
  <h3>Mailmodo Preview — ${email.id}</h3>
186
- <span>Style: ${email.style || 'branded'} · Press Ctrl+C in terminal to stop</span>
208
+ <span>Style: ${effectiveStyle} · Press Ctrl+C in terminal to stop</span>
187
209
  </div>
188
210
  <div class="email-frame">
189
211
  <div class="email-header">
@@ -194,11 +216,15 @@ export default class Preview extends BaseCommand {
194
216
  </div>
195
217
  </body>
196
218
  </html>`;
219
+ const port = await this.findAvailablePort(PREVIEW_PORT);
220
+ if (!jsonOutput && port !== PREVIEW_PORT) {
221
+ this.log(`\n ${chalk.yellow('!')} Port ${PREVIEW_PORT} is already in use. Opening preview on port ${chalk.cyan(String(port))}.`);
222
+ }
197
223
  if (jsonOutput) {
198
224
  this.log(JSON.stringify({
199
225
  id: email.id,
200
- style: email.style || 'branded',
201
- url: `http://localhost:${PREVIEW_PORT}`,
226
+ style: effectiveStyle,
227
+ url: `http://localhost:${port}`,
202
228
  }, null, 2));
203
229
  }
204
230
  const server = createServer((_req, res) => {
@@ -206,11 +232,11 @@ export default class Preview extends BaseCommand {
206
232
  res.end(wrapperHtml);
207
233
  });
208
234
  await new Promise((resolve) => {
209
- server.listen(PREVIEW_PORT, () => resolve());
235
+ server.listen(port, () => resolve());
210
236
  });
211
- const url = `http://localhost:${PREVIEW_PORT}`;
237
+ const url = `http://localhost:${port}`;
212
238
  if (!jsonOutput) {
213
- this.log(`\n Style: ${chalk.cyan(email.style || 'branded')}`);
239
+ this.log(`\n Style: ${chalk.cyan(effectiveStyle)}`);
214
240
  this.log(` Preview server at ${chalk.cyan(url)}`);
215
241
  this.log(` Opening in browser...\n`);
216
242
  this.log(` ${chalk.dim('Press Ctrl+C to stop the preview server.')}\n`);
@@ -68,4 +68,5 @@ export declare function saveTemplate(filename: string, html: string, cwd?: strin
68
68
  * or null if the file doesn't exist or can't be read.
69
69
  */
70
70
  export declare function loadTemplate(filename: string, cwd?: string): Promise<null | string>;
71
- export declare function resolveTemplateFilename(emailId: string, emailStyle?: 'branded' | 'plain', projectStyle?: 'branded' | 'plain'): string;
71
+ export declare function getEmailStyle(emailStyle?: 'branded' | 'plain', projectStyle?: 'branded' | 'plain'): 'branded' | 'plain';
72
+ export declare function getTemplateFilename(emailId: string, emailStyle?: 'branded' | 'plain', projectStyle?: 'branded' | 'plain'): string;
@@ -72,7 +72,10 @@ export async function loadTemplate(filename, cwd) {
72
72
  return null;
73
73
  }
74
74
  }
75
- export function resolveTemplateFilename(emailId, emailStyle, projectStyle) {
76
- const style = emailStyle ?? projectStyle ?? 'branded';
75
+ export function getEmailStyle(emailStyle, projectStyle) {
76
+ return emailStyle ?? projectStyle ?? 'branded';
77
+ }
78
+ export function getTemplateFilename(emailId, emailStyle, projectStyle) {
79
+ const style = getEmailStyle(emailStyle, projectStyle);
77
80
  return style === 'plain' ? `${emailId}_plain.html` : `${emailId}.html`;
78
81
  }
@@ -657,5 +657,5 @@
657
657
  ]
658
658
  }
659
659
  },
660
- "version": "0.0.30-beta.pr32.51"
660
+ "version": "0.0.30-beta.pr32.53"
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.30-beta.pr32.51",
4
+ "version": "0.0.30-beta.pr32.53",
5
5
  "author": "provishalk",
6
6
  "bin": {
7
7
  "mailmodo": "bin/run.js"