@mailmodo/cli 0.0.56-beta.pr58.98 → 0.0.56

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 } 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 { MISSING_TEMPLATES, restoredFromServerHint, } from '../../lib/messages.js';
6
+ import { MISSING_TEMPLATES } from '../../lib/messages.js';
7
7
  import { buildDeployPayload } from '../../lib/commands/deploy/payload.js';
8
8
  import { logDeploySuccessInstructions, logPreDeploySummary, } from '../../lib/commands/deploy/output.js';
9
9
  import { pauseSequence, resumeSequence, } from '../../lib/commands/deploy/sequence-status.js';
@@ -44,10 +44,8 @@ export default class Deploy extends BaseCommand {
44
44
  const yamlConfig = await this.ensureYaml();
45
45
  const missingIds = getMissingTemplateIds(yamlConfig);
46
46
  if (missingIds.length > 0) {
47
- const result = await handleMissingTemplates(ctx, yamlConfig, missingIds, baseFlags);
48
- if (result === 'restored')
49
- ctx.log(`\n ${chalk.green('✓')} ${restoredFromServerHint(missingIds)}\n`);
50
- else if (result === 'regenerated')
47
+ const regenerated = await handleMissingTemplates(ctx, yamlConfig, missingIds, baseFlags);
48
+ if (regenerated)
51
49
  ctx.log(`\n ${chalk.green('✓')} ${MISSING_TEMPLATES.REVIEW_HINT}\n`);
52
50
  return;
53
51
  }
@@ -71,7 +69,6 @@ export default class Deploy extends BaseCommand {
71
69
  if (!response.ok)
72
70
  ctx.onApiError(response);
73
71
  await ctx.syncYaml();
74
- await ctx.syncTemplates(yamlConfig);
75
72
  if (flags.json) {
76
73
  this.log(JSON.stringify({
77
74
  deployed: response.data.deployed,
@@ -89,7 +86,6 @@ export default class Deploy extends BaseCommand {
89
86
  collectDomainInputs: (yaml, skip) => this.collectDomainSetupInputs(yaml, skip),
90
87
  error: (msg) => this.error(msg),
91
88
  exit: (code) => this.exit(code),
92
- fetchTemplate: (emailId) => this.getTemplateFromServer(emailId),
93
89
  get: (path, params) => this.apiClient.get(path, params),
94
90
  getBillingCap: async () => {
95
91
  const s = await this.fetchBillingStatus();
@@ -101,7 +97,6 @@ export default class Deploy extends BaseCommand {
101
97
  registerDomainAndSave: (yaml, inputs, json) => this.registerDomain(yaml, inputs, json),
102
98
  showDnsRecords: (records, url, json) => this.logDnsRecords(records, url, json),
103
99
  spinner: (text, json, work) => this.withApiSpinner({ json, text }, work),
104
- syncTemplates: (yaml) => this.syncTemplatesToServer(yaml),
105
100
  syncYaml: () => this.syncYamlToServer(),
106
101
  };
107
102
  }
@@ -1,6 +1,6 @@
1
1
  import { Args, Flags } from '@oclif/core';
2
2
  import { BaseCommand } from '../../lib/base-command.js';
3
- import { getTemplateFilename, loadTemplate, } from '../../lib/yaml-config.js';
3
+ import { getTemplateFilename, loadTemplate } from '../../lib/yaml-config.js';
4
4
  import { handleMissingTemplates } from '../../lib/templates/missing-templates.js';
5
5
  import { askChangeDescription, runEditStep, } from '../../lib/commands/edit/flow.js';
6
6
  export default class Edit extends BaseCommand {
@@ -63,7 +63,6 @@ export default class Edit extends BaseCommand {
63
63
  post: (path, body) => this.apiClient.post(path, body),
64
64
  runCommand: (id, argv) => this.config.runCommand(id, argv),
65
65
  spinner: (text, json, work) => this.withApiSpinner({ json, text }, work),
66
- syncTemplate: (emailId) => this.syncTemplateToServer(emailId),
67
66
  syncYaml: () => this.syncYamlToServer(),
68
67
  };
69
68
  }
@@ -71,12 +70,10 @@ export default class Edit extends BaseCommand {
71
70
  return {
72
71
  error: (msg) => this.error(msg),
73
72
  exit: (code) => this.exit(code),
74
- fetchTemplate: (emailId) => this.getTemplateFromServer(emailId),
75
73
  log: (msg) => this.log(msg),
76
74
  onApiError: (r) => this.handleApiError(r),
77
75
  post: (path, body) => this.apiClient.post(path, body),
78
76
  spinner: (text, json, work) => this.withApiSpinner({ json, text }, work),
79
- syncTemplates: (yaml) => this.syncTemplatesToServer(yaml),
80
77
  syncYaml: () => this.syncYamlToServer(),
81
78
  };
82
79
  }
@@ -39,7 +39,6 @@ export default class Init extends BaseCommand {
39
39
  await applyMonthlyCap(ctx, yamlConfig);
40
40
  await saveAllTemplates(analysisPayload.recommendedEmails, generatedEmails);
41
41
  await ctx.syncYaml();
42
- await this.syncTemplatesToServer(yamlConfig);
43
42
  logInitSuccess(ctx, {
44
43
  brand: analysisPayload.brand,
45
44
  emailConfigs,
@@ -82,12 +82,10 @@ export default class Preview extends BaseCommand {
82
82
  return {
83
83
  error: (msg) => this.error(msg),
84
84
  exit: (code) => this.exit(code),
85
- fetchTemplate: (emailId) => this.getTemplateFromServer(emailId),
86
85
  log: (msg) => this.log(msg),
87
86
  onApiError: (r) => this.handleApiError(r),
88
87
  post: (path, body) => this.apiClient.post(path, body),
89
88
  spinner: (text, json, work) => this.withApiSpinner({ json, text }, work),
90
- syncTemplates: (yaml) => this.syncTemplatesToServer(yaml),
91
89
  syncYaml: () => this.syncYamlToServer(),
92
90
  };
93
91
  }
@@ -80,11 +80,8 @@ export declare abstract class BaseCommand extends Command {
80
80
  * successfully written, `false` otherwise (file already present, server 404,
81
81
  * or any network error). Silent — never throws.
82
82
  *
83
- * After ensuring the YAML is available, also silently restores any template
84
- * files that are missing from the local mailmodo/ folder, so a returning user
85
- * gets both their config and templates back without having to run init again.
86
- *
87
- * Used by `mailmodo login` right after the API key is validated.
83
+ * Used by `mailmodo login` right after the API key is validated so a returning
84
+ * user automatically gets their config back without having to run `init` again.
88
85
  */
89
86
  protected recoverYamlAfterLogin(client: ApiClient): Promise<boolean>;
90
87
  /**
@@ -93,34 +90,6 @@ export declare abstract class BaseCommand extends Command {
93
90
  * always succeeds regardless of sync failures.
94
91
  */
95
92
  protected syncYamlToServer(): Promise<void>;
96
- /**
97
- * Bulk-uploads all template HTML files referenced in the YAML to the server
98
- * as a backup. Best-effort: silently ignores all errors so the originating
99
- * command always succeeds regardless of sync failures.
100
- * Called after init, deploy, and AI regeneration.
101
- */
102
- protected syncTemplatesToServer(yaml: MailmodoYaml): Promise<void>;
103
- /**
104
- * Uploads a single template's HTML files to the server for incremental sync.
105
- * Best-effort: silently ignores all errors. Called after the edit command
106
- * applies changes to a specific template.
107
- */
108
- protected syncTemplateToServer(emailId: string): Promise<void>;
109
- /**
110
- * Fetches a single backed-up template from the server and writes it to the
111
- * local mailmodo/ folder. Returns true if the template was successfully
112
- * restored, false if it is not backed up or on any error.
113
- * Used as the `ctx.fetchTemplate` bridge passed to handleMissingTemplates.
114
- */
115
- protected getTemplateFromServer(emailId: string): Promise<boolean>;
116
- /**
117
- * Silently restores any template files missing from disk by fetching them
118
- * from the server. Uses the bulk endpoint when all templates are missing
119
- * (single round-trip), or individual per-ID requests when only a subset is
120
- * missing to avoid overwriting files that are already present. Best-effort —
121
- * never throws.
122
- */
123
- private fetchMissingTemplates;
124
93
  /**
125
94
  * Handles a failed API response by mapping HTTP status codes to
126
95
  * user-friendly error messages and exiting the process.
@@ -8,8 +8,6 @@ import ora from 'ora';
8
8
  import { ApiClient } from './api-client.js';
9
9
  import { loadConfig } from './config.js';
10
10
  import { API_ENDPOINTS, IS_DEV_MODE, YAML_FILE } from './constants.js';
11
- import { syncTemplatesToServer, syncTemplateToServer, fetchTemplatesFromServer, fetchTemplateFromServer, } from './templates/sync.js';
12
- import { getMissingTemplateIds } from './templates/missing-templates.js';
13
11
  import { ERRORS, INFO, PROMPTS, quotaExhaustedMessage, recordLabel, VALIDATION, } from './messages.js';
14
12
  import { loadYaml, saveYaml } from './yaml-config.js';
15
13
  export const FREE_TIER = 'free';
@@ -139,22 +137,13 @@ export class BaseCommand extends Command {
139
137
  * successfully written, `false` otherwise (file already present, server 404,
140
138
  * or any network error). Silent — never throws.
141
139
  *
142
- * After ensuring the YAML is available, also silently restores any template
143
- * files that are missing from the local mailmodo/ folder, so a returning user
144
- * gets both their config and templates back without having to run init again.
145
- *
146
- * Used by `mailmodo login` right after the API key is validated.
140
+ * Used by `mailmodo login` right after the API key is validated so a returning
141
+ * user automatically gets their config back without having to run `init` again.
147
142
  */
148
143
  async recoverYamlAfterLogin(client) {
149
- let yamlRestored = false;
150
- if (!existsSync(join(process.cwd(), YAML_FILE))) {
151
- yamlRestored = await this.fetchAndWriteYaml(client);
152
- }
153
- // Always check for missing templates, whether YAML was just restored or already present
154
- const yaml = await loadYaml();
155
- if (yaml)
156
- await this.fetchMissingTemplates(client, yaml);
157
- return yamlRestored;
144
+ if (existsSync(join(process.cwd(), YAML_FILE)))
145
+ return false;
146
+ return this.fetchAndWriteYaml(client);
158
147
  }
159
148
  /**
160
149
  * Uploads the current local mailmodo.yaml to the server as a backup.
@@ -184,92 +173,6 @@ export class BaseCommand extends Command {
184
173
  // Silently ignore — local file remains authoritative
185
174
  }
186
175
  }
187
- /**
188
- * Bulk-uploads all template HTML files referenced in the YAML to the server
189
- * as a backup. Best-effort: silently ignores all errors so the originating
190
- * command always succeeds regardless of sync failures.
191
- * Called after init, deploy, and AI regeneration.
192
- */
193
- async syncTemplatesToServer(yaml) {
194
- try {
195
- let client = this.apiClient;
196
- if (!client) {
197
- const envKey = process.env.MAILMODO_API_KEY;
198
- const apiKey = envKey ?? (await loadConfig())?.apiKey;
199
- if (!apiKey)
200
- return;
201
- client = new ApiClient(apiKey);
202
- }
203
- await syncTemplatesToServer(client, yaml);
204
- }
205
- catch {
206
- // Silently ignore — local files remain authoritative
207
- }
208
- }
209
- /**
210
- * Uploads a single template's HTML files to the server for incremental sync.
211
- * Best-effort: silently ignores all errors. Called after the edit command
212
- * applies changes to a specific template.
213
- */
214
- async syncTemplateToServer(emailId) {
215
- try {
216
- let client = this.apiClient;
217
- if (!client) {
218
- const envKey = process.env.MAILMODO_API_KEY;
219
- const apiKey = envKey ?? (await loadConfig())?.apiKey;
220
- if (!apiKey)
221
- return;
222
- client = new ApiClient(apiKey);
223
- }
224
- await syncTemplateToServer(client, emailId);
225
- }
226
- catch {
227
- // Silently ignore — local files remain authoritative
228
- }
229
- }
230
- /**
231
- * Fetches a single backed-up template from the server and writes it to the
232
- * local mailmodo/ folder. Returns true if the template was successfully
233
- * restored, false if it is not backed up or on any error.
234
- * Used as the `ctx.fetchTemplate` bridge passed to handleMissingTemplates.
235
- */
236
- async getTemplateFromServer(emailId) {
237
- try {
238
- let client = this.apiClient;
239
- if (!client) {
240
- const envKey = process.env.MAILMODO_API_KEY;
241
- const apiKey = envKey ?? (await loadConfig())?.apiKey;
242
- if (!apiKey)
243
- return false;
244
- client = new ApiClient(apiKey);
245
- }
246
- return fetchTemplateFromServer(client, emailId);
247
- }
248
- catch {
249
- return false;
250
- }
251
- }
252
- /**
253
- * Silently restores any template files missing from disk by fetching them
254
- * from the server. Uses the bulk endpoint when all templates are missing
255
- * (single round-trip), or individual per-ID requests when only a subset is
256
- * missing to avoid overwriting files that are already present. Best-effort —
257
- * never throws.
258
- */
259
- async fetchMissingTemplates(client, yaml) {
260
- try {
261
- const missingIds = getMissingTemplateIds(yaml);
262
- if (missingIds.length === 0)
263
- return;
264
- // Use bulk endpoint when every template is missing; per-ID otherwise
265
- await (missingIds.length === yaml.emails.length
266
- ? fetchTemplatesFromServer(client, yaml)
267
- : Promise.all(missingIds.map((id) => fetchTemplateFromServer(client, id))));
268
- }
269
- catch {
270
- // best-effort, silently ignore
271
- }
272
- }
273
176
  /**
274
177
  * Handles a failed API response by mapping HTTP status codes to
275
178
  * user-friendly error messages and exiting the process.
@@ -42,12 +42,12 @@ export function buildDiffPreview(email, updated, templateHtml) {
42
42
  diff.subject = subjectChanged
43
43
  ? { new: updated.subject, old: email.subject }
44
44
  : { unchanged: true, value: email.subject };
45
- if (email.previewText || updated.previewText) {
45
+ if (email.previewText ?? updated.previewText) {
46
46
  diff.previewText = previewChanged
47
47
  ? { new: updated.previewText, old: email.previewText }
48
48
  : { unchanged: true, value: email.previewText };
49
49
  }
50
- if (templateHtml || updated.html) {
50
+ if (templateHtml ?? updated.html) {
51
51
  const oldText = templateHtml
52
52
  ? truncate(stripHtml(templateHtml), 500)
53
53
  : null;
@@ -20,13 +20,12 @@ async function persistChanges(ctx, editCtx, updated) {
20
20
  await saveTemplate(editCtx.templateFilename, updated.html);
21
21
  }
22
22
  await ctx.syncYaml();
23
- await ctx.syncTemplate(editCtx.email.id);
24
23
  }
25
- function logJsonResult(ctx, email, updated, oldSubject, oldPreviewText) {
24
+ function logJsonResult(ctx, email, updated, oldSubject) {
26
25
  ctx.log(JSON.stringify({
27
26
  diff: {
28
- previewText: updated.previewText && updated.previewText !== oldPreviewText
29
- ? { new: updated.previewText, old: oldPreviewText }
27
+ previewText: updated.previewText && updated.previewText !== email.previewText
28
+ ? { new: updated.previewText, old: email.previewText }
30
29
  : undefined,
31
30
  subject: oldSubject === email.subject
32
31
  ? undefined
@@ -52,11 +51,10 @@ async function handleAcceptOutput(ctx, email) {
52
51
  export async function finalizeEdit(ctx, editCtx, opts) {
53
52
  const { flags, updated } = opts;
54
53
  const oldSubject = editCtx.email.subject;
55
- const oldPreviewText = editCtx.email.previewText;
56
54
  applyEmailChanges(editCtx.email, updated);
57
55
  await persistChanges(ctx, editCtx, updated);
58
56
  if (flags.json) {
59
- logJsonResult(ctx, editCtx.email, updated, oldSubject, oldPreviewText);
57
+ logJsonResult(ctx, editCtx.email, updated, oldSubject);
60
58
  }
61
59
  else if (flags.yes) {
62
60
  ctx.log(`\n Updated ${chalk.green('mailmodo.yaml')}\n`);
@@ -33,6 +33,5 @@ export type EditCtx = {
33
33
  post<T>(path: string, body?: unknown): Promise<ApiResponse<T>>;
34
34
  runCommand(id: string, argv: string[]): Promise<void>;
35
35
  spinner<T>(text: string, json: boolean, work: () => Promise<T>): Promise<T>;
36
- syncTemplate(emailId: string): Promise<void>;
37
36
  syncYaml(): Promise<void>;
38
37
  };
@@ -27,7 +27,7 @@ export async function openTemplateInEditor(ctx, template) {
27
27
  stdio: 'inherit',
28
28
  });
29
29
  child.on('error', () => resolve(false));
30
- child.on('close', (code) => resolve(code === 0));
30
+ child.on('close', () => resolve(true));
31
31
  });
32
32
  if (launched)
33
33
  return;
@@ -4,12 +4,8 @@ import { API_ENDPOINTS } from '../../constants.js';
4
4
  import { isValidUrl } from '../../utils.js';
5
5
  import { logAnalysisSummary } from './output.js';
6
6
  export async function promptProductUrl(flagUrl) {
7
- if (flagUrl) {
8
- if (!isValidUrl(flagUrl)) {
9
- throw new Error(`Invalid URL: "${flagUrl}". Please enter a valid URL (e.g., https://myapp.com)`);
10
- }
7
+ if (flagUrl)
11
8
  return flagUrl;
12
- }
13
9
  return input({
14
10
  message: 'What is your product URL?',
15
11
  validate(value) {
@@ -1,9 +1,7 @@
1
1
  import type { ApiResponse } from '../../api-client.js';
2
- import type { MailmodoYaml } from '../../yaml-config.js';
3
2
  export type PreviewCtx = {
4
3
  error(msg: string): never;
5
4
  exit(code?: number): never;
6
- fetchTemplate(emailId: string): Promise<boolean>;
7
5
  log(msg?: string): void;
8
6
  onApiError(resp: {
9
7
  error?: string;
@@ -11,7 +9,6 @@ export type PreviewCtx = {
11
9
  }): never;
12
10
  post<T>(path: string, body?: unknown): Promise<ApiResponse<T>>;
13
11
  spinner<T>(text: string, json: boolean, work: () => Promise<T>): Promise<T>;
14
- syncTemplates(yaml: MailmodoYaml): Promise<void>;
15
12
  syncYaml(): Promise<void>;
16
13
  };
17
14
  export type PreviewEmail = {
@@ -4,12 +4,11 @@
4
4
  * when this is true so end users never see internal request metadata.
5
5
  */
6
6
  export declare const IS_DEV_MODE: boolean;
7
- export declare const API_BASE_URL: string;
7
+ export declare const API_BASE_URL = "https://app-vertex-debug.azurewebsites.net";
8
8
  export declare const API_ENDPOINTS: Readonly<{
9
9
  ANALYTICS: "/analytics";
10
10
  ANALYZE: "/analyze";
11
11
  ASSETS_LOGO: "/assets/logo";
12
- ASSETS_TEMPLATES: "/assets/templates";
13
12
  ASSETS_YAML: "/assets/yaml";
14
13
  AUTH_VALIDATE: "/auth/validate";
15
14
  BILLING_CAP: "/billing/cap";
@@ -31,7 +30,7 @@ export declare const API_ENDPOINTS: Readonly<{
31
30
  SEQUENCES_SDK: "/sequences/sdk";
32
31
  SEQUENCES_VALIDATE: "/sequences/validate";
33
32
  }>;
34
- export declare const LOGIN_URL: string;
33
+ export declare const LOGIN_URL = "https://app-vertex-debug.azurewebsites.net/signup.html";
35
34
  export declare const PREVIEW_PORT = 3421;
36
35
  export declare const DEFAULT_BRAND_COLOR = "#1A56DB";
37
36
  export declare const TEMPLATES_DIR = "mailmodo";
@@ -1,12 +1,13 @@
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
- const PRODUCTION_API_BASE_URL = 'https://api.mailmodo.dev';
4
3
  /**
5
4
  * True when the CLI is running in a dev/debug context. Verbose request
6
5
  * diagnostics (URL, status, response body, error code) are only surfaced
7
6
  * when this is true so end users never see internal request metadata.
8
7
  */
9
8
  export const IS_DEV_MODE = Boolean(process.env.MAILMODO_DEV_TSX || process.env.MAILMODO_DEBUG);
9
+ // const PRODUCTION_API_BASE_URL = 'https://api.mailmodo.com';
10
+ const PRODUCTION_API_BASE_URL = 'https://app-vertex-debug.azurewebsites.net';
10
11
  export const API_BASE_URL = process.env.MAILMODO_DEV_TSX
11
12
  ? DEV_API_BASE_URL
12
13
  : PRODUCTION_API_BASE_URL;
@@ -14,7 +15,6 @@ export const API_ENDPOINTS = Object.freeze({
14
15
  ANALYTICS: '/analytics',
15
16
  ANALYZE: '/analyze',
16
17
  ASSETS_LOGO: '/assets/logo',
17
- ASSETS_TEMPLATES: '/assets/templates',
18
18
  ASSETS_YAML: '/assets/yaml',
19
19
  AUTH_VALIDATE: '/auth/validate',
20
20
  BILLING_CAP: '/billing/cap',
@@ -36,8 +36,9 @@ export const API_ENDPOINTS = Object.freeze({
36
36
  SEQUENCES_SDK: '/sequences/sdk',
37
37
  SEQUENCES_VALIDATE: '/sequences/validate',
38
38
  });
39
- const DEV_LOGIN_URL = 'https://app-vertex-debug.azurewebsites.net/signup';
40
- const PRODUCTION_LOGIN_URL = 'https://app.mailmodo.dev/signup';
39
+ const DEV_LOGIN_URL = 'https://app-vertex-debug.azurewebsites.net/signup.html';
40
+ // const PRODUCTION_LOGIN_URL = 'https://mailmodo.com/cli';
41
+ const PRODUCTION_LOGIN_URL = 'https://app-vertex-debug.azurewebsites.net/signup.html';
41
42
  export const LOGIN_URL = process.env.MAILMODO_DEV_TSX
42
43
  ? DEV_LOGIN_URL
43
44
  : PRODUCTION_LOGIN_URL;
@@ -63,7 +63,6 @@ export declare const DEPLOY: {
63
63
  readonly SDK_ONBOARDING_HEADER: string;
64
64
  readonly SUCCESS: `${string} Emails are live.`;
65
65
  };
66
- export declare function restoredFromServerHint(ids: string[]): string;
67
66
  export declare function pauseSuccess(sequenceId: string): string;
68
67
  export declare function pauseAlready(sequenceId: string): string;
69
68
  export declare function resumeSuccess(sequenceId: string): string;
@@ -63,16 +63,6 @@ export const DEPLOY = {
63
63
  SDK_ONBOARDING_HEADER: chalk.bold('ADD THIS TO YOUR APP (one-time only):'),
64
64
  SUCCESS: `${chalk.green('Deployed.')} Emails are live.`,
65
65
  };
66
- export function restoredFromServerHint(ids) {
67
- if (ids.length === 1) {
68
- const [id] = ids;
69
- return (`Template ${chalk.cyan(id)} was not found locally and has been refreshed from the server.\n` +
70
- ` Review it with ${chalk.cyan(`mailmodo preview ${id}`)}, then run ${chalk.cyan('mailmodo deploy')} again.`);
71
- }
72
- const list = ids.map((id) => chalk.cyan(id)).join(', ');
73
- return (`${ids.length} templates were not found locally and have been refreshed from the server: ${list}.\n` +
74
- ` Review each with ${chalk.cyan('mailmodo preview <id>')}, then run ${chalk.cyan('mailmodo deploy')} again.`);
75
- }
76
66
  export function pauseSuccess(sequenceId) {
77
67
  return `Sequence ${chalk.cyan(sequenceId)} paused. Run ${chalk.cyan(`mailmodo deploy --resume ${sequenceId}`)} to resume.`;
78
68
  }
@@ -1,19 +1,5 @@
1
1
  import { type MailmodoYaml } from '../yaml-config.js';
2
2
  import type { DeployFlags } from '../commands/deploy/types.js';
3
3
  import type { RegenCtx } from './types.js';
4
- /**
5
- * Returns the IDs of emails whose template file does not exist on disk.
6
- * Resolves the correct filename for each email by accounting for per-email
7
- * and project-level style settings (branded vs. plain).
8
- */
9
4
  export declare function getMissingTemplateIds(yamlConfig: MailmodoYaml): string[];
10
- /**
11
- * Handles the case where one or more template files are missing before a
12
- * command can proceed. First attempts a silent server restore for all missing
13
- * IDs; if the server has backups for all of them the command continues without
14
- * any user interaction. Only falls through to an interactive prompt (or an
15
- * immediate error in --json / --yes mode) for files the server also does not have.
16
- * Returns true if the situation was resolved (restored or regenerated), false
17
- * if the user chose to abort.
18
- */
19
- export declare function handleMissingTemplates(ctx: RegenCtx, yamlConfig: MailmodoYaml, missingIds: string[], flags: DeployFlags): Promise<'regenerated' | 'restored' | false>;
5
+ export declare function handleMissingTemplates(ctx: RegenCtx, yamlConfig: MailmodoYaml, missingIds: string[], flags: DeployFlags): Promise<boolean>;
@@ -2,56 +2,44 @@ import { existsSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { select } from '@inquirer/prompts';
4
4
  import chalk from 'chalk';
5
- import { TEMPLATES_DIR } from '../constants.js';
5
+ import { API_ENDPOINTS, TEMPLATES_DIR } from '../constants.js';
6
6
  import { MISSING_TEMPLATES } from '../messages.js';
7
- import { getTemplateFilename } from '../yaml-config.js';
8
- import { regenerateMissingTemplates } from './regenerate.js';
9
- /**
10
- * Returns the IDs of emails whose template file does not exist on disk.
11
- * Resolves the correct filename for each email by accounting for per-email
12
- * and project-level style settings (branded vs. plain).
13
- */
7
+ import { getTemplateFilename, saveTemplate, } from '../yaml-config.js';
8
+ import { buildRegeneratePayload } from '../commands/deploy/payload.js';
14
9
  export function getMissingTemplateIds(yamlConfig) {
15
10
  return yamlConfig.emails
16
11
  .filter((e) => !existsSync(join(process.cwd(), TEMPLATES_DIR, getTemplateFilename(e.id, e.style, yamlConfig.project?.emailStyle))))
17
12
  .map((e) => e.id);
18
13
  }
19
- /**
20
- * Attempts to restore each missing template from the server without showing
21
- * any prompt or message to the user. Returns the IDs that could not be
22
- * restored — either because the server has no backup or a network error occurred.
23
- */
24
- async function silentlyRestoreFromServer(ctx, missingIds) {
25
- const results = await Promise.all(missingIds.map((id) => ctx.fetchTemplate(id)));
26
- // Keep only the IDs where the server fetch returned false (not restored)
27
- return missingIds.filter((_, i) => !results[i]);
14
+ async function regenerateMissingTemplates(ctx, yamlConfig, missingIds, flags) {
15
+ const response = await ctx.spinner(` ${MISSING_TEMPLATES.REGENERATE_SPINNER}`, flags.json, () => ctx.post(API_ENDPOINTS.GENERATE, buildRegeneratePayload(yamlConfig, missingIds)));
16
+ if (!response.ok)
17
+ ctx.onApiError(response);
18
+ const saves = [];
19
+ for (const email of response.data?.emails ?? []) {
20
+ if (!/^[\w-]+$/.test(email.id))
21
+ continue;
22
+ if (email.html)
23
+ saves.push(saveTemplate(`${email.id}.html`, email.html));
24
+ if (email.plainHtml)
25
+ saves.push(saveTemplate(`${email.id}_plain.html`, email.plainHtml));
26
+ }
27
+ await Promise.all(saves);
28
+ await ctx.syncYaml();
28
29
  }
29
- /**
30
- * Handles the case where one or more template files are missing before a
31
- * command can proceed. First attempts a silent server restore for all missing
32
- * IDs; if the server has backups for all of them the command continues without
33
- * any user interaction. Only falls through to an interactive prompt (or an
34
- * immediate error in --json / --yes mode) for files the server also does not have.
35
- * Returns true if the situation was resolved (restored or regenerated), false
36
- * if the user chose to abort.
37
- */
38
30
  export async function handleMissingTemplates(ctx, yamlConfig, missingIds, flags) {
39
- // Try to silently recover from server before interrupting the user
40
- const stillMissing = await silentlyRestoreFromServer(ctx, missingIds);
41
- if (stillMissing.length === 0)
42
- return 'restored';
43
31
  if (flags.json) {
44
32
  ctx.log(JSON.stringify({
45
33
  error: 'missing_templates',
46
34
  message: MISSING_TEMPLATES.YES_ERROR,
47
- missingTemplates: stillMissing.map((id) => `${id}.html`),
35
+ missingTemplates: missingIds.map((id) => `${id}.html`),
48
36
  }, null, 2));
49
37
  ctx.exit(1);
50
38
  }
51
39
  if (flags.yes)
52
40
  ctx.error(MISSING_TEMPLATES.YES_ERROR);
53
41
  ctx.log(`\n ${MISSING_TEMPLATES.HEADER}`);
54
- for (const id of stillMissing)
42
+ for (const id of missingIds)
55
43
  ctx.log(` ${chalk.red('✗')} mailmodo/${id}.html`);
56
44
  ctx.log(`\n ${MISSING_TEMPLATES.REGENERATE_NOTE}\n`);
57
45
  const action = await select({
@@ -68,6 +56,6 @@ export async function handleMissingTemplates(ctx, yamlConfig, missingIds, flags)
68
56
  ctx.log(`\n ${MISSING_TEMPLATES.ABORT_HINT}\n`);
69
57
  return false;
70
58
  }
71
- await regenerateMissingTemplates(ctx, yamlConfig, stillMissing, flags);
72
- return 'regenerated';
59
+ await regenerateMissingTemplates(ctx, yamlConfig, missingIds, flags);
60
+ return true;
73
61
  }
@@ -1,9 +1,7 @@
1
1
  import type { ApiResponse } from '../api-client.js';
2
- import type { MailmodoYaml } from '../yaml-config.js';
3
2
  export type RegenCtx = {
4
3
  error(msg: string): never;
5
4
  exit(code?: number): never;
6
- fetchTemplate(emailId: string): Promise<boolean>;
7
5
  log(msg?: string): void;
8
6
  onApiError(resp: {
9
7
  error?: string;
@@ -11,6 +9,5 @@ export type RegenCtx = {
11
9
  }): never;
12
10
  post<T = Record<string, unknown>>(path: string, body?: Record<string, unknown> | unknown): Promise<ApiResponse<T>>;
13
11
  spinner<T>(text: string, json: boolean, work: () => Promise<T>): Promise<T>;
14
- syncTemplates(yaml: MailmodoYaml): Promise<void>;
15
12
  syncYaml(): Promise<void>;
16
13
  };
@@ -765,5 +765,5 @@
765
765
  ]
766
766
  }
767
767
  },
768
- "version": "0.0.56-beta.pr58.98"
768
+ "version": "0.0.56"
769
769
  }
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.56-beta.pr58.98",
4
+ "version": "0.0.56",
5
5
  "author": "provishalk",
6
6
  "bin": {
7
7
  "mailmodo": "bin/run.js"
@@ -1,10 +0,0 @@
1
- import { type MailmodoYaml } from '../yaml-config.js';
2
- import type { DeployFlags } from '../commands/deploy/types.js';
3
- import type { RegenCtx } from './types.js';
4
- /**
5
- * Calls POST /email/generate with the missing email IDs, writes the returned
6
- * HTML files to disk, then syncs both the YAML and all templates to the server.
7
- * Called when the user chooses "Re-generate via AI" from the missing-template
8
- * prompt, or when no server backup exists to restore from.
9
- */
10
- export declare function regenerateMissingTemplates(ctx: RegenCtx, yamlConfig: MailmodoYaml, missingIds: string[], flags: DeployFlags): Promise<void>;
@@ -1,29 +0,0 @@
1
- import { API_ENDPOINTS } from '../constants.js';
2
- import { saveTemplate } from '../yaml-config.js';
3
- import { buildRegeneratePayload } from '../commands/deploy/payload.js';
4
- import { MISSING_TEMPLATES } from '../messages.js';
5
- /**
6
- * Calls POST /email/generate with the missing email IDs, writes the returned
7
- * HTML files to disk, then syncs both the YAML and all templates to the server.
8
- * Called when the user chooses "Re-generate via AI" from the missing-template
9
- * prompt, or when no server backup exists to restore from.
10
- */
11
- export async function regenerateMissingTemplates(ctx, yamlConfig, missingIds, flags) {
12
- const response = await ctx.spinner(` ${MISSING_TEMPLATES.REGENERATE_SPINNER}`, flags.json, () => ctx.post(API_ENDPOINTS.GENERATE, buildRegeneratePayload(yamlConfig, missingIds)));
13
- if (!response.ok)
14
- ctx.onApiError(response);
15
- const saves = [];
16
- for (const email of response.data?.emails ?? []) {
17
- // Guard against path traversal: only allow alphanumeric, dash, and underscore IDs
18
- if (!/^[\w-]+$/.test(email.id))
19
- continue;
20
- if (email.html)
21
- saves.push(saveTemplate(`${email.id}.html`, email.html));
22
- if (email.plainHtml)
23
- saves.push(saveTemplate(`${email.id}_plain.html`, email.plainHtml));
24
- }
25
- await Promise.all(saves);
26
- await ctx.syncYaml();
27
- // Back up the freshly generated templates so the server reflects the new state
28
- await ctx.syncTemplates(yamlConfig);
29
- }
@@ -1,33 +0,0 @@
1
- import type { ApiClient } from '../api-client.js';
2
- import { type MailmodoYaml } from '../yaml-config.js';
3
- /**
4
- * Bulk-uploads all template HTML files referenced in the YAML to the server
5
- * as a backup. Only files present on disk are sent; any extra files in the
6
- * mailmodo/ folder that are not listed in the YAML are intentionally skipped.
7
- * All files are read in parallel before building the multipart payload.
8
- */
9
- export declare function syncTemplatesToServer(client: ApiClient, yaml: MailmodoYaml): Promise<void>;
10
- /**
11
- * Uploads a single template's HTML files to the server for incremental sync.
12
- * Used by the edit command after a change so only the modified template is
13
- * re-uploaded instead of the full set. The branded HTML (`html` field) is
14
- * required by the API; the plain variant (`plainHtml`) is uploaded only if
15
- * its file exists on disk.
16
- */
17
- export declare function syncTemplateToServer(client: ApiClient, emailId: string): Promise<void>;
18
- /**
19
- * Downloads all backed-up templates from the server and writes them to the
20
- * local mailmodo/ folder. Only templates whose emailId appears in the YAML
21
- * are written to disk — extra templates stored on the server (e.g. from a
22
- * previous project state) are intentionally ignored to keep the local folder
23
- * in sync with the current YAML.
24
- * Returns true if at least one file was written, false otherwise.
25
- */
26
- export declare function fetchTemplatesFromServer(client: ApiClient, yaml: MailmodoYaml): Promise<boolean>;
27
- /**
28
- * Downloads a single backed-up template from the server by emailId and writes
29
- * it to the local mailmodo/ folder. Returns true if the template was found and
30
- * written successfully, false if it has not been backed up or on any error.
31
- * The plain HTML variant is only written if the server returned a non-empty value.
32
- */
33
- export declare function fetchTemplateFromServer(client: ApiClient, emailId: string): Promise<boolean>;
@@ -1,106 +0,0 @@
1
- import { existsSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- import { readFile } from 'node:fs/promises';
4
- import { API_ENDPOINTS, TEMPLATES_DIR } from '../constants.js';
5
- import { saveTemplate } from '../yaml-config.js';
6
- /**
7
- * Bulk-uploads all template HTML files referenced in the YAML to the server
8
- * as a backup. Only files present on disk are sent; any extra files in the
9
- * mailmodo/ folder that are not listed in the YAML are intentionally skipped.
10
- * All files are read in parallel before building the multipart payload.
11
- */
12
- export async function syncTemplatesToServer(client, yaml) {
13
- // Collect filenames for both branded and plain variants, filtered to those that exist on disk
14
- const filenames = yaml.emails
15
- .flatMap((email) => [`${email.id}.html`, `${email.id}_plain.html`])
16
- .filter((f) => existsSync(join(process.cwd(), TEMPLATES_DIR, f)));
17
- if (filenames.length === 0)
18
- return;
19
- // Read all files in parallel to avoid sequential await-in-loop
20
- const files = await Promise.all(filenames.map(async (f) => ({
21
- content: await readFile(join(process.cwd(), TEMPLATES_DIR, f), 'utf8'),
22
- name: f,
23
- })));
24
- // API expects multipart fields keyed by the template filename (e.g. abc123.html)
25
- const formData = new FormData();
26
- for (const { name, content } of files) {
27
- formData.append(name, new Blob([content], { type: 'text/html' }), name);
28
- }
29
- await client.postFormData(API_ENDPOINTS.ASSETS_TEMPLATES, formData);
30
- }
31
- /**
32
- * Uploads a single template's HTML files to the server for incremental sync.
33
- * Used by the edit command after a change so only the modified template is
34
- * re-uploaded instead of the full set. The branded HTML (`html` field) is
35
- * required by the API; the plain variant (`plainHtml`) is uploaded only if
36
- * its file exists on disk.
37
- */
38
- export async function syncTemplateToServer(client, emailId) {
39
- const htmlPath = join(process.cwd(), TEMPLATES_DIR, `${emailId}.html`);
40
- // API requires the branded HTML field; skip entirely if the file is absent
41
- if (!existsSync(htmlPath))
42
- return;
43
- const formData = new FormData();
44
- const html = await readFile(htmlPath, 'utf8');
45
- formData.append('html', new Blob([html], { type: 'text/html' }), `${emailId}.html`);
46
- const plainPath = join(process.cwd(), TEMPLATES_DIR, `${emailId}_plain.html`);
47
- if (existsSync(plainPath)) {
48
- const plainHtml = await readFile(plainPath, 'utf8');
49
- formData.append('plainHtml', new Blob([plainHtml], { type: 'text/html' }), `${emailId}_plain.html`);
50
- }
51
- await client.postFormData(`${API_ENDPOINTS.ASSETS_TEMPLATES}/${emailId}`, formData);
52
- }
53
- /**
54
- * Downloads all backed-up templates from the server and writes them to the
55
- * local mailmodo/ folder. Only templates whose emailId appears in the YAML
56
- * are written to disk — extra templates stored on the server (e.g. from a
57
- * previous project state) are intentionally ignored to keep the local folder
58
- * in sync with the current YAML.
59
- * Returns true if at least one file was written, false otherwise.
60
- */
61
- export async function fetchTemplatesFromServer(client, yaml) {
62
- try {
63
- const resp = await client.get(API_ENDPOINTS.ASSETS_TEMPLATES);
64
- if (!resp.ok || !resp.data?.templates?.length)
65
- return false;
66
- // Build a set of valid IDs from the YAML to filter out stale server entries
67
- const validIds = new Set(yaml.emails.map((e) => e.id));
68
- const saves = [];
69
- for (const t of resp.data.templates) {
70
- if (!validIds.has(t.emailId))
71
- continue;
72
- // The API returns an empty string when a variant was never stored; skip those
73
- if (t.html)
74
- saves.push(saveTemplate(`${t.emailId}.html`, t.html));
75
- if (t.plainHtml)
76
- saves.push(saveTemplate(`${t.emailId}_plain.html`, t.plainHtml));
77
- }
78
- await Promise.all(saves);
79
- return saves.length > 0;
80
- }
81
- catch {
82
- return false;
83
- }
84
- }
85
- /**
86
- * Downloads a single backed-up template from the server by emailId and writes
87
- * it to the local mailmodo/ folder. Returns true if the template was found and
88
- * written successfully, false if it has not been backed up or on any error.
89
- * The plain HTML variant is only written if the server returned a non-empty value.
90
- */
91
- export async function fetchTemplateFromServer(client, emailId) {
92
- try {
93
- const resp = await client.get(`${API_ENDPOINTS.ASSETS_TEMPLATES}/${emailId}`);
94
- // A missing or empty html field means the template was never backed up
95
- if (!resp.ok || !resp.data?.html)
96
- return false;
97
- await saveTemplate(`${emailId}.html`, resp.data.html);
98
- // plainHtml is empty string when no plain variant is stored; skip in that case
99
- if (resp.data.plainHtml)
100
- await saveTemplate(`${emailId}_plain.html`, resp.data.plainHtml);
101
- return true;
102
- }
103
- catch {
104
- return false;
105
- }
106
- }