@mailmodo/cli 0.0.55 → 0.0.56-beta.pr58.100

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.
Files changed (136) hide show
  1. package/dist/commands/billing/index.d.ts +1 -11
  2. package/dist/commands/billing/index.js +28 -184
  3. package/dist/commands/contacts/index.d.ts +1 -19
  4. package/dist/commands/contacts/index.js +21 -114
  5. package/dist/commands/deploy/index.js +12 -7
  6. package/dist/commands/deployments/index.d.ts +1 -4
  7. package/dist/commands/deployments/index.js +11 -52
  8. package/dist/commands/domain/index.d.ts +1 -14
  9. package/dist/commands/domain/index.js +19 -100
  10. package/dist/commands/edit/index.d.ts +2 -20
  11. package/dist/commands/edit/index.js +33 -258
  12. package/dist/commands/emails/index.d.ts +1 -2
  13. package/dist/commands/emails/index.js +26 -91
  14. package/dist/commands/init/index.d.ts +1 -3
  15. package/dist/commands/init/index.js +47 -199
  16. package/dist/commands/login/index.d.ts +2 -0
  17. package/dist/commands/login/index.js +32 -79
  18. package/dist/commands/logs/index.d.ts +1 -8
  19. package/dist/commands/logs/index.js +12 -55
  20. package/dist/commands/preview/index.d.ts +1 -19
  21. package/dist/commands/preview/index.js +32 -212
  22. package/dist/commands/sdk/index.d.ts +1 -3
  23. package/dist/commands/sdk/index.js +14 -46
  24. package/dist/commands/settings/index.d.ts +1 -22
  25. package/dist/commands/settings/index.js +34 -246
  26. package/dist/commands/status/index.d.ts +1 -0
  27. package/dist/commands/status/index.js +13 -39
  28. package/dist/lib/base-command.d.ts +35 -10
  29. package/dist/lib/base-command.js +169 -17
  30. package/dist/lib/commands/billing/checkout-status.d.ts +3 -0
  31. package/dist/lib/commands/billing/checkout-status.js +63 -0
  32. package/dist/lib/commands/billing/format.d.ts +7 -0
  33. package/dist/lib/commands/billing/format.js +63 -0
  34. package/dist/lib/commands/billing/purchase-cap.d.ts +7 -0
  35. package/dist/lib/commands/billing/purchase-cap.js +57 -0
  36. package/dist/lib/commands/billing/types.d.ts +72 -0
  37. package/dist/lib/commands/contacts/actions.d.ts +3 -0
  38. package/dist/lib/commands/contacts/actions.js +49 -0
  39. package/dist/lib/commands/contacts/export-delete.d.ts +9 -0
  40. package/dist/lib/commands/contacts/export-delete.js +51 -0
  41. package/dist/lib/commands/contacts/types.d.ts +35 -0
  42. package/dist/lib/commands/contacts/types.js +1 -0
  43. package/dist/lib/{deploy → commands/deploy}/domain-setup.d.ts +1 -1
  44. package/dist/lib/{deploy → commands/deploy}/domain-setup.js +2 -2
  45. package/dist/lib/{deploy → commands/deploy}/output.d.ts +1 -1
  46. package/dist/lib/{deploy → commands/deploy}/output.js +2 -2
  47. package/dist/lib/{deploy → commands/deploy}/payload.d.ts +1 -1
  48. package/dist/lib/{deploy → commands/deploy}/payload.js +2 -2
  49. package/dist/lib/{deploy → commands/deploy}/sequence-status.js +2 -2
  50. package/dist/lib/{deploy → commands/deploy}/types.d.ts +4 -4
  51. package/dist/lib/commands/deploy/types.js +1 -0
  52. package/dist/lib/commands/deployments/output.d.ts +2 -0
  53. package/dist/lib/commands/deployments/output.js +68 -0
  54. package/dist/lib/commands/deployments/types.d.ts +24 -0
  55. package/dist/lib/commands/deployments/types.js +1 -0
  56. package/dist/lib/commands/domain/setup.d.ts +8 -0
  57. package/dist/lib/commands/domain/setup.js +53 -0
  58. package/dist/lib/commands/domain/types.d.ts +56 -0
  59. package/dist/lib/commands/domain/types.js +1 -0
  60. package/dist/lib/commands/domain/verify.d.ts +5 -0
  61. package/dist/lib/commands/domain/verify.js +50 -0
  62. package/dist/lib/commands/edit/diff.d.ts +7 -0
  63. package/dist/lib/commands/edit/diff.js +65 -0
  64. package/dist/lib/commands/edit/display.d.ts +5 -0
  65. package/dist/lib/commands/edit/display.js +53 -0
  66. package/dist/lib/commands/edit/flow.d.ts +8 -0
  67. package/dist/lib/commands/edit/flow.js +70 -0
  68. package/dist/lib/commands/edit/persist.d.ts +5 -0
  69. package/dist/lib/commands/edit/persist.js +67 -0
  70. package/dist/lib/commands/edit/types.d.ts +38 -0
  71. package/dist/lib/commands/edit/types.js +1 -0
  72. package/dist/lib/commands/emails/editor.d.ts +2 -0
  73. package/dist/lib/commands/emails/editor.js +43 -0
  74. package/dist/lib/commands/emails/output.d.ts +4 -0
  75. package/dist/lib/commands/emails/output.js +36 -0
  76. package/dist/lib/commands/emails/types.d.ts +3 -0
  77. package/dist/lib/commands/emails/types.js +1 -0
  78. package/dist/lib/commands/init/analysis.d.ts +3 -0
  79. package/dist/lib/commands/init/analysis.js +73 -0
  80. package/dist/lib/commands/init/output.d.ts +12 -0
  81. package/dist/lib/commands/init/output.js +39 -0
  82. package/dist/lib/commands/init/payload.d.ts +8 -0
  83. package/dist/lib/commands/init/payload.js +78 -0
  84. package/dist/lib/commands/init/types.d.ts +57 -0
  85. package/dist/lib/commands/init/types.js +1 -0
  86. package/dist/lib/commands/login/output.d.ts +8 -0
  87. package/dist/lib/commands/login/output.js +40 -0
  88. package/dist/lib/commands/login/types.d.ts +19 -0
  89. package/dist/lib/commands/login/types.js +1 -0
  90. package/dist/lib/commands/logs/output.d.ts +2 -0
  91. package/dist/lib/commands/logs/output.js +52 -0
  92. package/dist/lib/commands/logs/types.d.ts +23 -0
  93. package/dist/lib/commands/logs/types.js +1 -0
  94. package/dist/lib/commands/preview/actions.d.ts +11 -0
  95. package/dist/lib/commands/preview/actions.js +43 -0
  96. package/dist/lib/commands/preview/render.d.ts +3 -0
  97. package/dist/lib/commands/preview/render.js +30 -0
  98. package/dist/lib/commands/preview/server.d.ts +8 -0
  99. package/dist/lib/commands/preview/server.js +63 -0
  100. package/dist/lib/commands/preview/types.d.ts +22 -0
  101. package/dist/lib/commands/preview/types.js +1 -0
  102. package/dist/lib/commands/preview/wrapper-html.d.ts +2 -0
  103. package/dist/lib/commands/preview/wrapper-html.js +35 -0
  104. package/dist/lib/commands/sdk/output.d.ts +2 -0
  105. package/dist/lib/commands/sdk/output.js +42 -0
  106. package/dist/lib/commands/sdk/types.d.ts +21 -0
  107. package/dist/lib/commands/sdk/types.js +1 -0
  108. package/dist/lib/commands/settings/actions.d.ts +10 -0
  109. package/dist/lib/commands/settings/actions.js +56 -0
  110. package/dist/lib/commands/settings/display.d.ts +15 -0
  111. package/dist/lib/commands/settings/display.js +69 -0
  112. package/dist/lib/commands/settings/logo-domain.d.ts +3 -0
  113. package/dist/lib/commands/settings/logo-domain.js +47 -0
  114. package/dist/lib/commands/settings/prompt.d.ts +2 -0
  115. package/dist/lib/commands/settings/prompt.js +82 -0
  116. package/dist/lib/commands/settings/types.d.ts +65 -0
  117. package/dist/lib/commands/settings/types.js +1 -0
  118. package/dist/lib/commands/status/output.d.ts +2 -0
  119. package/dist/lib/commands/status/output.js +49 -0
  120. package/dist/lib/commands/status/types.d.ts +28 -0
  121. package/dist/lib/commands/status/types.js +1 -0
  122. package/dist/lib/constants.d.ts +3 -2
  123. package/dist/lib/constants.js +4 -5
  124. package/dist/lib/messages.d.ts +11 -0
  125. package/dist/lib/messages.js +31 -0
  126. package/dist/lib/templates/missing-templates.d.ts +16 -2
  127. package/dist/lib/templates/missing-templates.js +34 -22
  128. package/dist/lib/templates/regenerate.d.ts +10 -0
  129. package/dist/lib/templates/regenerate.js +29 -0
  130. package/dist/lib/templates/sync.d.ts +33 -0
  131. package/dist/lib/templates/sync.js +106 -0
  132. package/dist/lib/templates/types.d.ts +3 -0
  133. package/oclif.manifest.json +54 -54
  134. package/package.json +1 -1
  135. /package/dist/lib/{deploy → commands/billing}/types.js +0 -0
  136. /package/dist/lib/{deploy → commands/deploy}/sequence-status.d.ts +0 -0
@@ -1,27 +1,10 @@
1
1
  import { Flags } from '@oclif/core';
2
- import { confirm, editor, input, select } from '@inquirer/prompts';
3
- import chalk from 'chalk';
4
2
  import { BaseCommand } from '../../lib/base-command.js';
5
- import { API_ENDPOINTS, DEFAULT_BRAND_COLOR } from '../../lib/constants.js';
6
- import { loadYaml, saveTemplate, saveYaml, } from '../../lib/yaml-config.js';
7
- import { isValidUrl, normalizeTrigger } from '../../lib/utils.js';
8
- /**
9
- * Prints the human-readable analysis summary using the provided line writer.
10
- * Use stderr when `--json` is set so stdout stays free for machine-readable JSON.
11
- *
12
- * @param analysis - Parsed analyze API payload.
13
- * @param logLine - Line writer (typically bound `this.log` or `this.logToStderr`).
14
- */
15
- function logAnalysisSummary(analysis, logLine) {
16
- logLine(` Detected:`);
17
- logLine(` - Type: ${analysis.businessType} — ${analysis.description}`);
18
- logLine(` - Model: ${analysis.pricingModel}`);
19
- logLine(` - Users: ${analysis.targetUser}`);
20
- if (analysis.events?.length) {
21
- logLine(` - Events: ${analysis.events.join(', ')}`);
22
- }
23
- logLine('');
24
- }
3
+ import { API_ENDPOINTS } from '../../lib/constants.js';
4
+ import { loadYaml, saveYaml } from '../../lib/yaml-config.js';
5
+ import { analyzeProduct, promptProductUrl, } from '../../lib/commands/init/analysis.js';
6
+ import { confirmOverwrite, logInitSuccess, } from '../../lib/commands/init/output.js';
7
+ import { applyMonthlyCap, buildEmailConfigs, buildYamlConfig, preserveUserFields, saveAllTemplates, } from '../../lib/commands/init/payload.js';
25
8
  export default class Init extends BaseCommand {
26
9
  static description = 'Analyze your product and generate email sequences';
27
10
  static examples = [
@@ -35,189 +18,54 @@ export default class Init extends BaseCommand {
35
18
  async run() {
36
19
  const { flags } = await this.parse(Init);
37
20
  await this.ensureAuth();
38
- const existing = await loadYaml();
39
- if (!(await this.confirmOverwriteIfNeeded(flags, existing)))
40
- return;
41
- let productUrl = flags.url;
42
- if (!productUrl) {
43
- productUrl = await input({
44
- message: 'What is your product URL?',
45
- validate(value) {
46
- if (!value?.trim())
47
- return 'URL is required';
48
- if (isValidUrl(value))
49
- return true;
50
- return 'Please enter a valid URL (e.g., https://myapp.com)';
51
- },
52
- });
53
- }
54
- const analysisResponse = await this.withApiSpinner({
55
- json: flags.json,
56
- text: ' Analyzing your product — scraping homepage, pricing, features',
57
- }, () => this.apiClient.post(API_ENDPOINTS.ANALYZE, {
58
- url: productUrl,
59
- }));
60
- if (!analysisResponse.ok) {
61
- this.handleAiQuotaError(analysisResponse, 'init');
62
- this.handleApiError(analysisResponse);
63
- }
64
- const analysis = analysisResponse.data;
65
- const shouldShowAnalysisSummary = !flags.json || !flags.yes;
66
- if (shouldShowAnalysisSummary) {
67
- const logSummary = flags.json && !flags.yes
68
- ? this.logToStderr.bind(this)
69
- : this.log.bind(this);
70
- logAnalysisSummary(analysis, logSummary);
71
- }
72
- let analysisPayload = analysis;
73
- if (!flags.yes) {
74
- const userAction = await select({
75
- choices: [
76
- { name: 'Yes - continue with this result', value: 'yes' },
77
- { name: 'No - stop here', value: 'no' },
78
- { name: 'Edit - update the result before continuing', value: 'edit' },
79
- ],
80
- message: 'Does this look right?',
81
- });
82
- if (userAction === 'no') {
83
- this.log(`\n Stopped. Run ${chalk.cyan('mailmodo init')} again when you are ready.\n`);
21
+ const ctx = this.makeCtx();
22
+ const baseFlags = { json: flags.json, yes: flags.yes };
23
+ if (this.isBlankDirectory()) {
24
+ const restored = await this.promptInitServerRestore(baseFlags);
25
+ if (restored)
84
26
  return;
85
- }
86
- if (userAction === 'edit') {
87
- const editedAnalysis = await editor({
88
- default: JSON.stringify(analysis, null, 2),
89
- message: 'Edit the analysis JSON. Save and close to continue.',
90
- postfix: '.json',
91
- validate(value) {
92
- if (!value?.trim())
93
- return 'Edited analysis cannot be empty';
94
- try {
95
- JSON.parse(value);
96
- return true;
97
- }
98
- catch {
99
- return 'Please provide valid JSON';
100
- }
101
- },
102
- });
103
- analysisPayload = JSON.parse(editedAnalysis);
104
- }
105
- }
106
- const generateResponse = await this.withApiSpinner({
107
- json: flags.json,
108
- text: ' Generating email templates...',
109
- }, () => this.apiClient.post(API_ENDPOINTS.GENERATE, {
110
- ...analysisPayload,
111
- }));
112
- if (!generateResponse.ok) {
113
- this.handleApiError(generateResponse);
114
27
  }
28
+ const existing = await loadYaml();
29
+ if (!(await confirmOverwrite(ctx, baseFlags, existing)))
30
+ return;
31
+ const productUrl = await promptProductUrl(flags.url);
32
+ const analysisPayload = await analyzeProduct(ctx, productUrl, baseFlags);
33
+ if (!analysisPayload)
34
+ return;
35
+ const generateResponse = await ctx.spinner(' Generating email templates...', flags.json, () => ctx.post(API_ENDPOINTS.GENERATE, { ...analysisPayload }));
36
+ if (!generateResponse.ok)
37
+ ctx.onApiError(generateResponse);
115
38
  const generatedEmails = generateResponse.data?.emails || [];
116
- const emailConfigs = analysisPayload.recommendedEmails.map((rec, index) => {
117
- const generated = generatedEmails[index];
118
- return {
119
- delay: rec.delay || '0',
120
- id: rec.id,
121
- trigger: normalizeTrigger(rec.trigger, analysisPayload.productName),
122
- ...(rec.condition ? { condition: rec.condition } : {}),
123
- subject: generated?.subject || `Email for ${rec.id}`,
124
- template: `mailmodo/${rec.id}.html`,
125
- ...(generated?.previewText
126
- ? { previewText: generated.previewText }
127
- : {}),
128
- ...(generated?.ctaText ? { ctaText: generated.ctaText } : {}),
129
- goal: rec.goal,
130
- };
131
- });
132
- const yamlConfig = {
133
- emails: emailConfigs,
134
- project: {
135
- brandColor: analysisPayload.brand?.color || DEFAULT_BRAND_COLOR,
136
- description: analysisPayload.description,
137
- emailStyle: 'plain',
138
- fromEmail: '',
139
- fromName: `Team ${analysisPayload.productName}`,
140
- logoUrl: analysisPayload.brand?.logoUrl || '',
141
- name: analysisPayload.productName,
142
- pricingModel: analysisPayload.pricingModel,
143
- replyTo: '',
144
- saasModel: analysisPayload.saasModel,
145
- targetUser: analysisPayload.targetUser,
146
- type: analysisPayload.businessType,
147
- url: productUrl,
148
- webhookUrl: '',
149
- },
150
- };
39
+ const emailConfigs = buildEmailConfigs(analysisPayload, generatedEmails);
40
+ const yamlConfig = buildYamlConfig(analysisPayload, emailConfigs, productUrl);
151
41
  if (existing)
152
- this.preserveUserFields(yamlConfig, existing);
42
+ preserveUserFields(yamlConfig, existing);
153
43
  await saveYaml(yamlConfig);
154
- await this.persistMonthlyCap(yamlConfig);
155
- const templateSaves = analysisPayload.recommendedEmails.flatMap((rec, index) => {
156
- const generated = generatedEmails[index];
157
- const saves = [];
158
- if (generated?.html) {
159
- saves.push(saveTemplate(`${rec.id}.html`, generated.html));
160
- }
161
- if (generated?.plainHtml) {
162
- saves.push(saveTemplate(`${rec.id}_plain.html`, generated.plainHtml));
163
- }
164
- return saves;
44
+ await applyMonthlyCap(ctx, yamlConfig);
45
+ await saveAllTemplates(analysisPayload.recommendedEmails, generatedEmails);
46
+ await ctx.syncYaml();
47
+ await this.syncTemplatesToServer(yamlConfig);
48
+ logInitSuccess(ctx, {
49
+ brand: analysisPayload.brand,
50
+ emailConfigs,
51
+ emailStyle: yamlConfig.project.emailStyle,
52
+ json: flags.json,
165
53
  });
166
- await Promise.all(templateSaves);
167
- await this.syncYamlToServer();
168
- if (flags.json) {
169
- this.log(JSON.stringify({
170
- brandDetected: analysisPayload.brand,
171
- emails: emailConfigs,
172
- emailsCreated: emailConfigs.length,
173
- style: yamlConfig.project.emailStyle,
174
- }, null, 2));
175
- return;
176
- }
177
- this.log(` Created ${chalk.green('mailmodo.yaml')} + ${chalk.green(String(emailConfigs.length))} email templates in ${chalk.green('/mailmodo')}\n`);
178
- this.log(` Run ${chalk.cyan("'mailmodo emails'")} to review.\n`);
179
54
  }
180
- async persistMonthlyCap(yamlConfig) {
181
- const billingStatus = await this.fetchBillingStatus();
182
- const monthlyCap = billingStatus?.cap?.inBlocks;
183
- if (monthlyCap !== null && monthlyCap !== undefined) {
184
- yamlConfig.project.monthlyCap = monthlyCap;
185
- await saveYaml(yamlConfig);
186
- }
187
- }
188
- preserveUserFields(config, existing) {
189
- const p = existing.project;
190
- if (p.fromEmail)
191
- config.project.fromEmail = p.fromEmail;
192
- if (p.replyTo)
193
- config.project.replyTo = p.replyTo;
194
- if (p.fromName)
195
- config.project.fromName = p.fromName;
196
- if (p.webhookUrl)
197
- config.project.webhookUrl = p.webhookUrl;
198
- if (p.emailStyle)
199
- config.project.emailStyle = p.emailStyle;
200
- if (p.domain)
201
- config.project.domain = p.domain;
202
- if (p.address)
203
- config.project.address = p.address;
204
- if (p.logoFile)
205
- config.project.logoFile = p.logoFile;
206
- }
207
- async confirmOverwriteIfNeeded(flags, existing) {
208
- if (!existing)
209
- return true;
210
- if (flags.yes)
211
- return true;
212
- this.log(`\n ${chalk.yellow('⚠')} ${chalk.bold('mailmodo.yaml already exists.')}`);
213
- this.log(` Running init will overwrite your current project configuration and all email templates.\n`);
214
- const proceed = await confirm({
215
- default: false,
216
- message: 'Overwrite existing configuration and templates?',
217
- });
218
- if (!proceed) {
219
- this.log(`\n Init cancelled. Run ${chalk.cyan('mailmodo edit')} to modify individual emails.\n`);
220
- }
221
- return proceed;
55
+ makeCtx() {
56
+ return {
57
+ get: (path, params) => this.apiClient.get(path, params),
58
+ getBillingCap: async () => {
59
+ const s = await this.fetchBillingStatus();
60
+ return s?.cap?.inBlocks ?? undefined;
61
+ },
62
+ log: (msg) => this.log(msg),
63
+ logToStderr: (msg) => this.logToStderr(msg ?? ''),
64
+ onAiQuotaError: (r, feature) => this.handleAiQuotaError(r, feature),
65
+ onApiError: (r) => this.handleApiError(r),
66
+ post: (path, body) => this.apiClient.post(path, body),
67
+ spinner: (text, json, work) => this.withApiSpinner({ json, text }, work),
68
+ syncYaml: () => this.syncYamlToServer(),
69
+ };
222
70
  }
223
71
  }
@@ -7,4 +7,6 @@ export default class Login extends BaseCommand {
7
7
  yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
8
  };
9
9
  run(): Promise<void>;
10
+ private makeCtx;
11
+ private promptForApiKey;
10
12
  }
@@ -6,6 +6,7 @@ import { ApiClient } from '../../lib/api-client.js';
6
6
  import { loadConfig, saveConfig } from '../../lib/config.js';
7
7
  import { API_ENDPOINTS, LOGIN_URL } from '../../lib/constants.js';
8
8
  import { INFO } from '../../lib/messages.js';
9
+ import { logAlreadyLoggedIn, logLoginSuccess, } from '../../lib/commands/login/output.js';
9
10
  export default class Login extends BaseCommand {
10
11
  static description = 'Authenticate with Mailmodo using your API key';
11
12
  static examples = process.platform === 'win32'
@@ -18,100 +19,52 @@ export default class Login extends BaseCommand {
18
19
  '<%= config.bin %> login',
19
20
  'MAILMODO_API_KEY=YOUR_API_KEY <%= config.bin %> login',
20
21
  ];
21
- static flags = {
22
- ...BaseCommand.baseFlags,
23
- };
22
+ static flags = { ...BaseCommand.baseFlags };
24
23
  async run() {
25
24
  const { flags } = await this.parse(Login);
25
+ const ctx = this.makeCtx();
26
26
  const envKey = process.env.MAILMODO_API_KEY;
27
27
  if (!envKey) {
28
28
  const existing = await loadConfig();
29
29
  if (existing?.apiKey) {
30
- const existingClient = new ApiClient(existing.apiKey);
31
- const yamlRestored = await this.recoverYamlAfterLogin(existingClient);
32
- if (flags.json) {
33
- this.log(JSON.stringify({
34
- email: existing.email ?? null,
35
- status: 'already_logged_in',
36
- totalFreeRemaining: existing.totalFreeRemaining ?? null,
37
- yamlRestored,
38
- }, null, 2));
39
- return;
40
- }
41
- this.log('\n You are already logged in.\n');
42
- const emailDisplay = existing.email?.trim()
43
- ? chalk.green(existing.email.trim())
44
- : chalk.dim('(unknown)');
45
- this.log(` Email: ${emailDisplay}\n`);
46
- if (yamlRestored) {
47
- this.log(` ${INFO.YAML_RESTORED_ON_LOGIN}`);
48
- }
49
- else {
50
- this.log(` ${chalk.dim('1.')} Run ${chalk.cyan('mailmodo init')} to generate an email sequence.`);
51
- }
52
- this.log(` ${chalk.dim(yamlRestored ? '1.' : '2.')} Run ${chalk.cyan('mailmodo logout')} to log in with another account.\n`);
30
+ logAlreadyLoggedIn(ctx, existing, { json: flags.json });
53
31
  return;
54
32
  }
55
33
  }
56
- let apiKey = envKey;
57
- if (apiKey) {
58
- this.log('Detected MAILMODO_API_KEY from environment.');
59
- }
60
- else {
61
- this.log(`\n Get your free API key at: ${chalk.cyan(LOGIN_URL)}\n`);
62
- try {
63
- await open(LOGIN_URL);
64
- this.log(` ${INFO.BROWSER_OPENING}\n`);
65
- }
66
- catch {
67
- this.log(` ${INFO.BROWSER_OPEN_FAILED}\n`);
68
- }
69
- apiKey = await input({
70
- message: 'Paste your API key:',
71
- validate(value) {
72
- if (!value?.trim())
73
- return 'API key is required';
74
- return true;
75
- },
76
- });
77
- }
34
+ const apiKey = envKey ?? (await this.promptForApiKey());
78
35
  const trimmedKey = apiKey.trim();
79
36
  const client = new ApiClient(trimmedKey);
80
- const response = await this.withApiSpinner({ json: flags.json, text: ' Validating API key...' }, () => client.get(API_ENDPOINTS.AUTH_VALIDATE));
37
+ const response = await ctx.spinner(' Validating API key...', flags.json, () => client.get(API_ENDPOINTS.AUTH_VALIDATE));
81
38
  if (!response.ok) {
82
- this.handleApiError(response);
39
+ ctx.onApiError(response);
83
40
  }
84
41
  const { email, totalFreeRemaining, paidEmailsRemaining, plan } = response.data;
85
- await saveConfig({
86
- apiKey: trimmedKey,
87
- email,
88
- totalFreeRemaining,
89
- });
90
- const yamlRestored = await this.recoverYamlAfterLogin(client);
91
- if (flags.json) {
92
- this.log(JSON.stringify({
93
- email,
94
- plan,
95
- totalFreeRemaining,
96
- paidEmailsRemaining,
97
- status: 'authenticated',
98
- yamlRestored,
99
- }, null, 2));
100
- return;
101
- }
102
- this.log(`\n Logged in as ${chalk.green(email)}`);
103
- if (plan === 'free') {
104
- this.log(` Free tier: ${chalk.cyan(String(totalFreeRemaining))} emails remaining`);
105
- this.log(' No credit card required.\n');
106
- }
107
- if (plan === 'paid') {
108
- this.log(` Current paid block: ${chalk.cyan(String(paidEmailsRemaining))} emails remaining\n`);
109
- }
110
- if (yamlRestored) {
111
- this.log(` ${INFO.YAML_RESTORED_ON_LOGIN}\n`);
42
+ await saveConfig({ apiKey: trimmedKey, email, totalFreeRemaining });
43
+ logLoginSuccess(ctx, { email, paidEmailsRemaining, plan, totalFreeRemaining }, { json: flags.json });
44
+ }
45
+ makeCtx() {
46
+ return {
47
+ log: (msg) => this.log(msg),
48
+ onApiError: (r) => this.handleApiError(r),
49
+ spinner: (text, json, work) => this.withApiSpinner({ json, text }, work),
50
+ };
51
+ }
52
+ async promptForApiKey() {
53
+ this.log(`\n Get your free API key at: ${chalk.cyan(LOGIN_URL)}\n`);
54
+ try {
55
+ await open(LOGIN_URL);
56
+ this.log(` ${INFO.BROWSER_OPENING}\n`);
112
57
  }
113
- else {
114
- this.log(` Next: Run ${chalk.cyan("'mailmodo init'")} to generate your email sequences.\n`);
58
+ catch {
59
+ this.log(` ${INFO.BROWSER_OPEN_FAILED}\n`);
115
60
  }
61
+ return input({
62
+ message: 'Paste your API key:',
63
+ validate(value) {
64
+ if (!value?.trim())
65
+ return 'API key is required';
66
+ return true;
67
+ },
68
+ });
116
69
  }
117
70
  }
@@ -11,12 +11,5 @@ export default class Logs extends BaseCommand {
11
11
  yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
12
  };
13
13
  run(): Promise<void>;
14
- /**
15
- * Returns the appropriate chalk color function based on the delivery status.
16
- * Green for sent/opened/clicked, red for bounced/failed, yellow for skipped.
17
- *
18
- * @param {string} status - The delivery event status string.
19
- * @returns {Function} A chalk color function to wrap the status display text.
20
- */
21
- private statusColor;
14
+ private makeCtx;
22
15
  }
@@ -1,7 +1,7 @@
1
1
  import { Flags } from '@oclif/core';
2
- import chalk from 'chalk';
3
2
  import { BaseCommand } from '../../lib/base-command.js';
4
3
  import { API_ENDPOINTS } from '../../lib/constants.js';
4
+ import { renderLogs } from '../../lib/commands/logs/output.js';
5
5
  export default class Logs extends BaseCommand {
6
6
  static description = 'View email send logs and delivery events';
7
7
  static examples = [
@@ -29,6 +29,7 @@ export default class Logs extends BaseCommand {
29
29
  async run() {
30
30
  const { flags } = await this.parse(Logs);
31
31
  await this.ensureAuth();
32
+ const ctx = this.makeCtx();
32
33
  const params = {
33
34
  limit: String(flags.limit),
34
35
  page: String(flags.page),
@@ -37,66 +38,22 @@ export default class Logs extends BaseCommand {
37
38
  params.email = flags.email;
38
39
  if (flags.failed)
39
40
  params.failed = 'true';
40
- const response = await this.withApiSpinner({ json: flags.json, text: ' Loading email logs...' }, () => this.apiClient.get(API_ENDPOINTS.LOGS, params));
41
+ const response = await ctx.spinner(' Loading email logs...', flags.json, () => ctx.get(API_ENDPOINTS.LOGS, params));
41
42
  if (!response.ok) {
42
- this.handleApiError(response);
43
+ ctx.onApiError(response);
43
44
  }
44
- const { entries, limit, page, total } = response.data;
45
45
  if (flags.json) {
46
46
  this.log(JSON.stringify(response.data, null, 2));
47
47
  return;
48
48
  }
49
- this.log(`\n ${'Time'.padEnd(18)}${'Email'.padEnd(24)}${'Status'.padEnd(10)}Contact`);
50
- this.log(` ${'─'.repeat(68)}`);
51
- if (entries?.length) {
52
- for (const entry of entries) {
53
- const time = (entry.timestamp || '').padEnd(18);
54
- const templateId = (entry.emailId || '').padEnd(24);
55
- const statusColor = this.statusColor(entry.status);
56
- const status = statusColor((entry.status || '').padEnd(10));
57
- const contact = entry.contact || '';
58
- this.log(` ${time}${templateId}${status}${contact}`);
59
- if (entry.reason) {
60
- const label = entry.status === 'skipped' ? 'condition not met' : 'reason';
61
- this.log(` ${' '.repeat(52)}${chalk.dim(`(${label}: ${entry.reason})`)}`);
62
- }
63
- }
64
- const totalPages = Math.ceil(total / limit);
65
- this.log(`\n Page ${page} of ${totalPages} · ${total} total entries`);
66
- if (page < totalPages) {
67
- this.log(` ${chalk.dim(`Next: --page ${page + 1}`)}`);
68
- }
69
- }
70
- else {
71
- this.log(` ${chalk.dim('No log entries found.')}`);
72
- }
73
- this.log('');
49
+ renderLogs(ctx, response.data);
74
50
  }
75
- /**
76
- * Returns the appropriate chalk color function based on the delivery status.
77
- * Green for sent/opened/clicked, red for bounced/failed, yellow for skipped.
78
- *
79
- * @param {string} status - The delivery event status string.
80
- * @returns {Function} A chalk color function to wrap the status display text.
81
- */
82
- statusColor(status) {
83
- switch (status) {
84
- case 'bounced':
85
- case 'complained':
86
- case 'failed': {
87
- return chalk.red;
88
- }
89
- case 'clicked':
90
- case 'opened':
91
- case 'sent': {
92
- return chalk.green;
93
- }
94
- case 'skipped': {
95
- return chalk.yellow;
96
- }
97
- default: {
98
- return chalk.white;
99
- }
100
- }
51
+ makeCtx() {
52
+ return {
53
+ get: (path, params) => this.apiClient.get(path, params),
54
+ log: (msg) => this.log(msg),
55
+ onApiError: (r) => this.handleApiError(r),
56
+ spinner: (text, json, work) => this.withApiSpinner({ json, text }, work),
57
+ };
101
58
  }
102
59
  }
@@ -12,23 +12,5 @@ export default class Preview extends BaseCommand {
12
12
  yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
13
  };
14
14
  run(): Promise<void>;
15
- /**
16
- * Renders a plain text version of the email to stdout. Used by AI agents
17
- * and CI pipelines that cannot open a browser.
18
- */
19
- private renderText;
20
- /**
21
- * Calls the API to send a test email to the specified address.
22
- * Before domain verification, tests send via the mailmodo.com domain.
23
- */
24
- private sendTestEmail;
25
- /**
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.
32
- */
33
- private startPreviewServer;
15
+ private makeCtx;
34
16
  }