@mailmodo/cli 0.0.55 → 0.0.56-beta.pr58.101
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/billing/index.d.ts +1 -11
- package/dist/commands/billing/index.js +28 -184
- package/dist/commands/contacts/index.d.ts +1 -19
- package/dist/commands/contacts/index.js +21 -114
- package/dist/commands/deploy/index.js +12 -7
- package/dist/commands/deployments/index.d.ts +1 -4
- package/dist/commands/deployments/index.js +11 -52
- package/dist/commands/domain/index.d.ts +1 -14
- package/dist/commands/domain/index.js +19 -100
- package/dist/commands/edit/index.d.ts +2 -20
- package/dist/commands/edit/index.js +33 -258
- package/dist/commands/emails/index.d.ts +1 -2
- package/dist/commands/emails/index.js +26 -91
- package/dist/commands/init/index.d.ts +1 -3
- package/dist/commands/init/index.js +51 -200
- package/dist/commands/login/index.d.ts +2 -0
- package/dist/commands/login/index.js +32 -79
- package/dist/commands/logs/index.d.ts +1 -8
- package/dist/commands/logs/index.js +12 -55
- package/dist/commands/preview/index.d.ts +1 -19
- package/dist/commands/preview/index.js +32 -212
- package/dist/commands/sdk/index.d.ts +1 -3
- package/dist/commands/sdk/index.js +14 -46
- package/dist/commands/settings/index.d.ts +1 -22
- package/dist/commands/settings/index.js +34 -246
- package/dist/commands/status/index.d.ts +1 -0
- package/dist/commands/status/index.js +13 -39
- package/dist/lib/base-command.d.ts +38 -10
- package/dist/lib/base-command.js +171 -18
- package/dist/lib/commands/billing/checkout-status.d.ts +3 -0
- package/dist/lib/commands/billing/checkout-status.js +63 -0
- package/dist/lib/commands/billing/format.d.ts +7 -0
- package/dist/lib/commands/billing/format.js +63 -0
- package/dist/lib/commands/billing/purchase-cap.d.ts +7 -0
- package/dist/lib/commands/billing/purchase-cap.js +57 -0
- package/dist/lib/commands/billing/types.d.ts +72 -0
- package/dist/lib/commands/contacts/actions.d.ts +3 -0
- package/dist/lib/commands/contacts/actions.js +49 -0
- package/dist/lib/commands/contacts/export-delete.d.ts +9 -0
- package/dist/lib/commands/contacts/export-delete.js +51 -0
- package/dist/lib/commands/contacts/types.d.ts +35 -0
- package/dist/lib/commands/contacts/types.js +1 -0
- package/dist/lib/{deploy → commands/deploy}/domain-setup.d.ts +1 -1
- package/dist/lib/{deploy → commands/deploy}/domain-setup.js +2 -2
- package/dist/lib/{deploy → commands/deploy}/output.d.ts +1 -1
- package/dist/lib/{deploy → commands/deploy}/output.js +2 -2
- package/dist/lib/{deploy → commands/deploy}/payload.d.ts +1 -1
- package/dist/lib/{deploy → commands/deploy}/payload.js +2 -2
- package/dist/lib/{deploy → commands/deploy}/sequence-status.js +2 -2
- package/dist/lib/{deploy → commands/deploy}/types.d.ts +4 -4
- package/dist/lib/commands/deploy/types.js +1 -0
- package/dist/lib/commands/deployments/output.d.ts +2 -0
- package/dist/lib/commands/deployments/output.js +68 -0
- package/dist/lib/commands/deployments/types.d.ts +24 -0
- package/dist/lib/commands/deployments/types.js +1 -0
- package/dist/lib/commands/domain/setup.d.ts +8 -0
- package/dist/lib/commands/domain/setup.js +53 -0
- package/dist/lib/commands/domain/types.d.ts +56 -0
- package/dist/lib/commands/domain/types.js +1 -0
- package/dist/lib/commands/domain/verify.d.ts +5 -0
- package/dist/lib/commands/domain/verify.js +50 -0
- package/dist/lib/commands/edit/diff.d.ts +7 -0
- package/dist/lib/commands/edit/diff.js +65 -0
- package/dist/lib/commands/edit/display.d.ts +5 -0
- package/dist/lib/commands/edit/display.js +53 -0
- package/dist/lib/commands/edit/flow.d.ts +8 -0
- package/dist/lib/commands/edit/flow.js +70 -0
- package/dist/lib/commands/edit/persist.d.ts +5 -0
- package/dist/lib/commands/edit/persist.js +67 -0
- package/dist/lib/commands/edit/types.d.ts +38 -0
- package/dist/lib/commands/edit/types.js +1 -0
- package/dist/lib/commands/emails/editor.d.ts +2 -0
- package/dist/lib/commands/emails/editor.js +43 -0
- package/dist/lib/commands/emails/output.d.ts +4 -0
- package/dist/lib/commands/emails/output.js +36 -0
- package/dist/lib/commands/emails/types.d.ts +3 -0
- package/dist/lib/commands/emails/types.js +1 -0
- package/dist/lib/commands/init/analysis.d.ts +3 -0
- package/dist/lib/commands/init/analysis.js +73 -0
- package/dist/lib/commands/init/output.d.ts +12 -0
- package/dist/lib/commands/init/output.js +39 -0
- package/dist/lib/commands/init/payload.d.ts +8 -0
- package/dist/lib/commands/init/payload.js +78 -0
- package/dist/lib/commands/init/types.d.ts +57 -0
- package/dist/lib/commands/init/types.js +1 -0
- package/dist/lib/commands/login/output.d.ts +8 -0
- package/dist/lib/commands/login/output.js +40 -0
- package/dist/lib/commands/login/types.d.ts +19 -0
- package/dist/lib/commands/login/types.js +1 -0
- package/dist/lib/commands/logs/output.d.ts +2 -0
- package/dist/lib/commands/logs/output.js +52 -0
- package/dist/lib/commands/logs/types.d.ts +23 -0
- package/dist/lib/commands/logs/types.js +1 -0
- package/dist/lib/commands/preview/actions.d.ts +11 -0
- package/dist/lib/commands/preview/actions.js +43 -0
- package/dist/lib/commands/preview/render.d.ts +3 -0
- package/dist/lib/commands/preview/render.js +30 -0
- package/dist/lib/commands/preview/server.d.ts +8 -0
- package/dist/lib/commands/preview/server.js +63 -0
- package/dist/lib/commands/preview/types.d.ts +22 -0
- package/dist/lib/commands/preview/types.js +1 -0
- package/dist/lib/commands/preview/wrapper-html.d.ts +2 -0
- package/dist/lib/commands/preview/wrapper-html.js +35 -0
- package/dist/lib/commands/sdk/output.d.ts +2 -0
- package/dist/lib/commands/sdk/output.js +42 -0
- package/dist/lib/commands/sdk/types.d.ts +21 -0
- package/dist/lib/commands/sdk/types.js +1 -0
- package/dist/lib/commands/settings/actions.d.ts +10 -0
- package/dist/lib/commands/settings/actions.js +56 -0
- package/dist/lib/commands/settings/display.d.ts +15 -0
- package/dist/lib/commands/settings/display.js +69 -0
- package/dist/lib/commands/settings/logo-domain.d.ts +3 -0
- package/dist/lib/commands/settings/logo-domain.js +47 -0
- package/dist/lib/commands/settings/prompt.d.ts +2 -0
- package/dist/lib/commands/settings/prompt.js +82 -0
- package/dist/lib/commands/settings/types.d.ts +65 -0
- package/dist/lib/commands/settings/types.js +1 -0
- package/dist/lib/commands/status/output.d.ts +2 -0
- package/dist/lib/commands/status/output.js +49 -0
- package/dist/lib/commands/status/types.d.ts +28 -0
- package/dist/lib/commands/status/types.js +1 -0
- package/dist/lib/constants.d.ts +3 -2
- package/dist/lib/constants.js +4 -5
- package/dist/lib/messages.d.ts +11 -0
- package/dist/lib/messages.js +31 -0
- package/dist/lib/templates/missing-templates.d.ts +16 -2
- package/dist/lib/templates/missing-templates.js +34 -22
- package/dist/lib/templates/regenerate.d.ts +10 -0
- package/dist/lib/templates/regenerate.js +29 -0
- package/dist/lib/templates/sync.d.ts +33 -0
- package/dist/lib/templates/sync.js +106 -0
- package/dist/lib/templates/types.d.ts +3 -0
- package/dist/lib/yaml-config.d.ts +1 -0
- package/dist/lib/yaml-config.js +8 -0
- package/oclif.manifest.json +100 -100
- package/package.json +1 -1
- /package/dist/lib/{deploy → commands/billing}/types.js +0 -0
- /package/dist/lib/{deploy → commands/deploy}/sequence-status.d.ts +0 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ApiResponse } from '../../api-client.js';
|
|
2
|
+
export interface EmailMetric {
|
|
3
|
+
click: string;
|
|
4
|
+
conv: string;
|
|
5
|
+
emailId: string;
|
|
6
|
+
open: string;
|
|
7
|
+
sent: number;
|
|
8
|
+
}
|
|
9
|
+
export interface AnalyticsResponse {
|
|
10
|
+
emails: EmailMetric[];
|
|
11
|
+
monthlySent: number;
|
|
12
|
+
quota: {
|
|
13
|
+
blockSize: number;
|
|
14
|
+
blocksUsed: number;
|
|
15
|
+
currentBlockEmailsRemaining: number;
|
|
16
|
+
freeRemaining: number;
|
|
17
|
+
plan: string;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export type StatusCtx = {
|
|
21
|
+
get<T = Record<string, unknown>>(path: string, params?: Record<string, string>): Promise<ApiResponse<T>>;
|
|
22
|
+
log(msg?: string): void;
|
|
23
|
+
onApiError(resp: {
|
|
24
|
+
error?: string;
|
|
25
|
+
status: number;
|
|
26
|
+
}): never;
|
|
27
|
+
spinner<T>(text: string, json: boolean, work: () => Promise<T>): Promise<T>;
|
|
28
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/lib/constants.d.ts
CHANGED
|
@@ -4,11 +4,12 @@
|
|
|
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
|
|
7
|
+
export declare const API_BASE_URL: string;
|
|
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";
|
|
12
13
|
ASSETS_YAML: "/assets/yaml";
|
|
13
14
|
AUTH_VALIDATE: "/auth/validate";
|
|
14
15
|
BILLING_CAP: "/billing/cap";
|
|
@@ -30,7 +31,7 @@ export declare const API_ENDPOINTS: Readonly<{
|
|
|
30
31
|
SEQUENCES_SDK: "/sequences/sdk";
|
|
31
32
|
SEQUENCES_VALIDATE: "/sequences/validate";
|
|
32
33
|
}>;
|
|
33
|
-
export declare const LOGIN_URL
|
|
34
|
+
export declare const LOGIN_URL: string;
|
|
34
35
|
export declare const PREVIEW_PORT = 3421;
|
|
35
36
|
export declare const DEFAULT_BRAND_COLOR = "#1A56DB";
|
|
36
37
|
export declare const TEMPLATES_DIR = "mailmodo";
|
package/dist/lib/constants.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
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';
|
|
3
4
|
/**
|
|
4
5
|
* True when the CLI is running in a dev/debug context. Verbose request
|
|
5
6
|
* diagnostics (URL, status, response body, error code) are only surfaced
|
|
6
7
|
* when this is true so end users never see internal request metadata.
|
|
7
8
|
*/
|
|
8
9
|
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';
|
|
11
10
|
export const API_BASE_URL = process.env.MAILMODO_DEV_TSX
|
|
12
11
|
? DEV_API_BASE_URL
|
|
13
12
|
: PRODUCTION_API_BASE_URL;
|
|
@@ -15,6 +14,7 @@ export const API_ENDPOINTS = Object.freeze({
|
|
|
15
14
|
ANALYTICS: '/analytics',
|
|
16
15
|
ANALYZE: '/analyze',
|
|
17
16
|
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,9 +36,8 @@ 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
|
-
|
|
41
|
-
const PRODUCTION_LOGIN_URL = 'https://app-vertex-debug.azurewebsites.net/signup.html';
|
|
39
|
+
const DEV_LOGIN_URL = 'https://app-vertex-debug.azurewebsites.net/signup';
|
|
40
|
+
const PRODUCTION_LOGIN_URL = 'https://app.mailmodo.dev/signup';
|
|
42
41
|
export const LOGIN_URL = process.env.MAILMODO_DEV_TSX
|
|
43
42
|
? DEV_LOGIN_URL
|
|
44
43
|
: PRODUCTION_LOGIN_URL;
|
package/dist/lib/messages.d.ts
CHANGED
|
@@ -13,6 +13,16 @@ export declare const PROMPTS: {
|
|
|
13
13
|
readonly REPLY_TO: "Reply-to address (optional, press Enter to use sender email):";
|
|
14
14
|
readonly SENDER_EMAIL: "Sender email address:";
|
|
15
15
|
};
|
|
16
|
+
export declare const BLANK_DIR: {
|
|
17
|
+
readonly CHOICE_FRESH: "Start fresh — new emails will be generated (your domain, sender, and address settings will be kept)";
|
|
18
|
+
readonly CHOICE_RESTORE: "Restore project files from server";
|
|
19
|
+
readonly CHOICE_RESTORE_INIT: "Restore existing project files from server";
|
|
20
|
+
readonly CHOICE_SKIP: "Skip — I'll navigate to my project directory manually";
|
|
21
|
+
readonly PROMPT: "No mailmodo.yaml or templates found in this directory.";
|
|
22
|
+
readonly PROMPT_INIT: "A project was found on the server. What would you like to do?";
|
|
23
|
+
readonly RESTORED_INIT: `Project restored from server. Run ${string} to re-deploy, or ${string} to review your emails.`;
|
|
24
|
+
readonly SKIP_HINT: `Navigate to your project directory and run your command there, or run ${string} to start a new project here.`;
|
|
25
|
+
};
|
|
16
26
|
export declare const MISSING_TEMPLATES: {
|
|
17
27
|
readonly ABORT_HINT: `Restore the missing files from version control, then run ${string} again.`;
|
|
18
28
|
readonly CHOICE_ABORT: "Abort (restore from version control)";
|
|
@@ -63,6 +73,7 @@ export declare const DEPLOY: {
|
|
|
63
73
|
readonly SDK_ONBOARDING_HEADER: string;
|
|
64
74
|
readonly SUCCESS: `${string} Emails are live.`;
|
|
65
75
|
};
|
|
76
|
+
export declare function restoredFromServerHint(ids: string[]): string;
|
|
66
77
|
export declare function pauseSuccess(sequenceId: string): string;
|
|
67
78
|
export declare function pauseAlready(sequenceId: string): string;
|
|
68
79
|
export declare function resumeSuccess(sequenceId: string): string;
|
package/dist/lib/messages.js
CHANGED
|
@@ -14,6 +14,16 @@ export const PROMPTS = {
|
|
|
14
14
|
REPLY_TO: 'Reply-to address (optional, press Enter to use sender email):',
|
|
15
15
|
SENDER_EMAIL: 'Sender email address:',
|
|
16
16
|
};
|
|
17
|
+
export const BLANK_DIR = {
|
|
18
|
+
CHOICE_FRESH: 'Start fresh — new emails will be generated (your domain, sender, and address settings will be kept)',
|
|
19
|
+
CHOICE_RESTORE: 'Restore project files from server',
|
|
20
|
+
CHOICE_RESTORE_INIT: 'Restore existing project files from server',
|
|
21
|
+
CHOICE_SKIP: "Skip — I'll navigate to my project directory manually",
|
|
22
|
+
PROMPT: 'No mailmodo.yaml or templates found in this directory.',
|
|
23
|
+
PROMPT_INIT: 'A project was found on the server. What would you like to do?',
|
|
24
|
+
RESTORED_INIT: `Project restored from server. Run ${chalk.cyan('mailmodo deploy')} to re-deploy, or ${chalk.cyan('mailmodo emails')} to review your emails.`,
|
|
25
|
+
SKIP_HINT: `Navigate to your project directory and run your command there, or run ${chalk.cyan('mailmodo init')} to start a new project here.`,
|
|
26
|
+
};
|
|
17
27
|
export const MISSING_TEMPLATES = {
|
|
18
28
|
ABORT_HINT: `Restore the missing files from version control, then run ${chalk.cyan('mailmodo deploy')} again.`,
|
|
19
29
|
CHOICE_ABORT: 'Abort (restore from version control)',
|
|
@@ -63,6 +73,27 @@ export const DEPLOY = {
|
|
|
63
73
|
SDK_ONBOARDING_HEADER: chalk.bold('ADD THIS TO YOUR APP (one-time only):'),
|
|
64
74
|
SUCCESS: `${chalk.green('Deployed.')} Emails are live.`,
|
|
65
75
|
};
|
|
76
|
+
export function restoredFromServerHint(ids) {
|
|
77
|
+
if (ids.length === 1) {
|
|
78
|
+
const [id] = ids;
|
|
79
|
+
return (`Template ${chalk.cyan(id)} was not found locally and has been refreshed from the server.\n` +
|
|
80
|
+
` Review it with ${chalk.cyan(`mailmodo preview ${id}`)}, then run ${chalk.cyan('mailmodo deploy')} again.`);
|
|
81
|
+
}
|
|
82
|
+
const header = 'Template ID';
|
|
83
|
+
const colWidth = Math.max(header.length, ...ids.map((id) => id.length));
|
|
84
|
+
const pad = (s) => s.padEnd(colWidth);
|
|
85
|
+
const hr = '─'.repeat(colWidth + 2);
|
|
86
|
+
const table = [
|
|
87
|
+
` ┌${hr}┐`,
|
|
88
|
+
` │ ${chalk.bold(pad(header))} │`,
|
|
89
|
+
` ├${hr}┤`,
|
|
90
|
+
...ids.map((id) => ` │ ${chalk.cyan(pad(id))} │`),
|
|
91
|
+
` └${hr}┘`,
|
|
92
|
+
].join('\n');
|
|
93
|
+
return (`${ids.length} templates were not found locally and have been refreshed from the server:\n\n` +
|
|
94
|
+
`${table}\n\n` +
|
|
95
|
+
` Review each with ${chalk.cyan('mailmodo preview <id>')}, then run ${chalk.cyan('mailmodo deploy')} again.`);
|
|
96
|
+
}
|
|
66
97
|
export function pauseSuccess(sequenceId) {
|
|
67
98
|
return `Sequence ${chalk.cyan(sequenceId)} paused. Run ${chalk.cyan(`mailmodo deploy --resume ${sequenceId}`)} to resume.`;
|
|
68
99
|
}
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import { type MailmodoYaml } from '../yaml-config.js';
|
|
2
|
-
import type { DeployFlags } from '../deploy/types.js';
|
|
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
|
+
*/
|
|
4
9
|
export declare function getMissingTemplateIds(yamlConfig: MailmodoYaml): string[];
|
|
5
|
-
|
|
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>;
|
|
@@ -2,44 +2,56 @@ 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 {
|
|
5
|
+
import { TEMPLATES_DIR } from '../constants.js';
|
|
6
6
|
import { MISSING_TEMPLATES } from '../messages.js';
|
|
7
|
-
import { getTemplateFilename
|
|
8
|
-
import {
|
|
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
|
+
*/
|
|
9
14
|
export function getMissingTemplateIds(yamlConfig) {
|
|
10
15
|
return yamlConfig.emails
|
|
11
16
|
.filter((e) => !existsSync(join(process.cwd(), TEMPLATES_DIR, getTemplateFilename(e.id, e.style, yamlConfig.project?.emailStyle))))
|
|
12
17
|
.map((e) => e.id);
|
|
13
18
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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();
|
|
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]);
|
|
29
28
|
}
|
|
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
|
+
*/
|
|
30
38
|
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';
|
|
31
43
|
if (flags.json) {
|
|
32
44
|
ctx.log(JSON.stringify({
|
|
33
45
|
error: 'missing_templates',
|
|
34
46
|
message: MISSING_TEMPLATES.YES_ERROR,
|
|
35
|
-
missingTemplates:
|
|
47
|
+
missingTemplates: stillMissing.map((id) => `${id}.html`),
|
|
36
48
|
}, null, 2));
|
|
37
49
|
ctx.exit(1);
|
|
38
50
|
}
|
|
39
51
|
if (flags.yes)
|
|
40
52
|
ctx.error(MISSING_TEMPLATES.YES_ERROR);
|
|
41
53
|
ctx.log(`\n ${MISSING_TEMPLATES.HEADER}`);
|
|
42
|
-
for (const id of
|
|
54
|
+
for (const id of stillMissing)
|
|
43
55
|
ctx.log(` ${chalk.red('✗')} mailmodo/${id}.html`);
|
|
44
56
|
ctx.log(`\n ${MISSING_TEMPLATES.REGENERATE_NOTE}\n`);
|
|
45
57
|
const action = await select({
|
|
@@ -56,6 +68,6 @@ export async function handleMissingTemplates(ctx, yamlConfig, missingIds, flags)
|
|
|
56
68
|
ctx.log(`\n ${MISSING_TEMPLATES.ABORT_HINT}\n`);
|
|
57
69
|
return false;
|
|
58
70
|
}
|
|
59
|
-
await regenerateMissingTemplates(ctx, yamlConfig,
|
|
60
|
-
return
|
|
71
|
+
await regenerateMissingTemplates(ctx, yamlConfig, stillMissing, flags);
|
|
72
|
+
return 'regenerated';
|
|
61
73
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
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>;
|
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
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>;
|
|
@@ -0,0 +1,106 @@
|
|
|
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
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { ApiResponse } from '../api-client.js';
|
|
2
|
+
import type { MailmodoYaml } from '../yaml-config.js';
|
|
2
3
|
export type RegenCtx = {
|
|
3
4
|
error(msg: string): never;
|
|
4
5
|
exit(code?: number): never;
|
|
6
|
+
fetchTemplate(emailId: string): Promise<boolean>;
|
|
5
7
|
log(msg?: string): void;
|
|
6
8
|
onApiError(resp: {
|
|
7
9
|
error?: string;
|
|
@@ -9,5 +11,6 @@ export type RegenCtx = {
|
|
|
9
11
|
}): never;
|
|
10
12
|
post<T = Record<string, unknown>>(path: string, body?: Record<string, unknown> | unknown): Promise<ApiResponse<T>>;
|
|
11
13
|
spinner<T>(text: string, json: boolean, work: () => Promise<T>): Promise<T>;
|
|
14
|
+
syncTemplates(yaml: MailmodoYaml): Promise<void>;
|
|
12
15
|
syncYaml(): Promise<void>;
|
|
13
16
|
};
|
|
@@ -44,6 +44,7 @@ export interface MailmodoYaml {
|
|
|
44
44
|
* formatted Error with the line number if the file contains invalid YAML syntax.
|
|
45
45
|
*/
|
|
46
46
|
export declare function loadYaml(cwd?: string): Promise<MailmodoYaml | null>;
|
|
47
|
+
export declare function parseYamlText(text: string): MailmodoYaml | null;
|
|
47
48
|
/**
|
|
48
49
|
* Serializes and writes the mailmodo.yaml configuration to disk.
|
|
49
50
|
*
|
package/dist/lib/yaml-config.js
CHANGED
|
@@ -25,6 +25,14 @@ export async function loadYaml(cwd) {
|
|
|
25
25
|
throw new Error(yamlParseError(error.message));
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
+
export function parseYamlText(text) {
|
|
29
|
+
try {
|
|
30
|
+
return load(text);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
28
36
|
/**
|
|
29
37
|
* Serializes and writes the mailmodo.yaml configuration to disk.
|
|
30
38
|
*
|