sendcraft-cli 1.0.0 → 1.2.0
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/README.md +551 -0
- package/bin/sendcraft.js +26 -8
- package/lib/banner.js +53 -0
- package/lib/client.js +87 -1
- package/lib/commands/campaigns.js +16 -15
- package/lib/commands/config.js +23 -15
- package/lib/commands/domains.js +49 -45
- package/lib/commands/emails.js +35 -31
- package/lib/commands/keys.js +32 -28
- package/lib/commands/login.js +206 -0
- package/lib/commands/mcp.js +77 -0
- package/lib/commands/send.js +8 -7
- package/lib/commands/subscribers.js +20 -21
- package/lib/commands/warmup.js +40 -19
- package/lib/config.js +21 -1
- package/lib/interactive.js +410 -0
- package/lib/output.js +28 -6
- package/package.json +10 -2
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive TUI — shown when `sendcraft` is run with no arguments.
|
|
3
|
+
* Uses prompts for arrow-key navigation through menus.
|
|
4
|
+
*/
|
|
5
|
+
const prompts = require('prompts');
|
|
6
|
+
const chalk = require('chalk');
|
|
7
|
+
const _grad = require('gradient-string'); const gradient = _grad.default || _grad;
|
|
8
|
+
const { banner, sendcraftGradient } = require('./banner');
|
|
9
|
+
const client = require('./client');
|
|
10
|
+
const { table, colorStatus, info, success, error, spinner } = require('./output');
|
|
11
|
+
const pkg = require('../package.json');
|
|
12
|
+
|
|
13
|
+
const sendcraftColor = gradient(['#6366f1', '#8b5cf6', '#ec4899']);
|
|
14
|
+
|
|
15
|
+
// ─── Menus ────────────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
const MAIN_MENU = [
|
|
18
|
+
{ title: '📧 ' + chalk.bold('Send Email'), value: 'send' },
|
|
19
|
+
{ title: '📋 ' + chalk.bold('Emails'), value: 'emails' },
|
|
20
|
+
{ title: '📣 ' + chalk.bold('Campaigns'), value: 'campaigns' },
|
|
21
|
+
{ title: '👥 ' + chalk.bold('Subscribers'), value: 'subscribers' },
|
|
22
|
+
{ title: '🌐 ' + chalk.bold('Domains'), value: 'domains' },
|
|
23
|
+
{ title: '🔑 ' + chalk.bold('API Keys'), value: 'keys' },
|
|
24
|
+
{ title: '🔥 ' + chalk.bold('SMTP Warmup'), value: 'warmup' },
|
|
25
|
+
{ title: '🤖 ' + chalk.bold('MCP Setup'), value: 'mcp' },
|
|
26
|
+
{ title: '🔐 ' + chalk.bold('Login'), value: 'login' },
|
|
27
|
+
{ title: '⚙️ ' + chalk.bold('Config'), value: 'config' },
|
|
28
|
+
{ title: chalk.dim('✕ Exit'), value: 'exit' },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
// ─── Screens ──────────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
async function screenSendEmail() {
|
|
34
|
+
const answers = await prompts([
|
|
35
|
+
{ type: 'text', name: 'to', message: 'Recipient email' },
|
|
36
|
+
{ type: 'text', name: 'subject', message: 'Subject' },
|
|
37
|
+
{ type: 'text', name: 'html', message: 'HTML body (or press Enter to use text)' },
|
|
38
|
+
{ type: 'text', name: 'text', message: 'Plain text body' },
|
|
39
|
+
{ type: 'text', name: 'from', message: 'From address (leave blank for default)' },
|
|
40
|
+
], { onCancel: () => null });
|
|
41
|
+
|
|
42
|
+
if (!answers.to || !answers.subject) return;
|
|
43
|
+
|
|
44
|
+
const sp = spinner(`Sending to ${chalk.cyan(answers.to)}…`).start();
|
|
45
|
+
try {
|
|
46
|
+
const result = await client.post('/emails/send', {
|
|
47
|
+
toEmail: answers.to,
|
|
48
|
+
subject: answers.subject,
|
|
49
|
+
htmlContent: answers.html || undefined,
|
|
50
|
+
plainTextContent: answers.text || undefined,
|
|
51
|
+
fromEmail: answers.from || undefined,
|
|
52
|
+
});
|
|
53
|
+
sp.succeed(chalk.dim('Done'));
|
|
54
|
+
success(`Email sent! ${chalk.dim('ID: ' + (result.emailId || result._id || '—'))}`);
|
|
55
|
+
} catch (e) {
|
|
56
|
+
sp.fail(chalk.red('Failed'));
|
|
57
|
+
error(e.message);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function screenEmails() {
|
|
62
|
+
const { action } = await prompts({
|
|
63
|
+
type: 'select',
|
|
64
|
+
name: 'action',
|
|
65
|
+
message: 'Emails',
|
|
66
|
+
choices: [
|
|
67
|
+
{ title: 'List emails', value: 'list' },
|
|
68
|
+
{ title: 'Stats summary', value: 'stats' },
|
|
69
|
+
{ title: chalk.dim('← Back'), value: 'back' },
|
|
70
|
+
],
|
|
71
|
+
});
|
|
72
|
+
if (!action || action === 'back') return;
|
|
73
|
+
|
|
74
|
+
if (action === 'list') {
|
|
75
|
+
const sp = spinner('Fetching emails…').start();
|
|
76
|
+
try {
|
|
77
|
+
const data = await client.get('/emails?page=1&limit=20');
|
|
78
|
+
sp.succeed(chalk.dim('Loaded'));
|
|
79
|
+
const emails = data.emails || [];
|
|
80
|
+
if (!emails.length) return info('No emails found.');
|
|
81
|
+
table(
|
|
82
|
+
['ID', 'To', 'Subject', 'Status', 'Sent At'],
|
|
83
|
+
emails.map(e => [
|
|
84
|
+
chalk.dim(String(e._id).slice(-8)),
|
|
85
|
+
chalk.cyan(e.toEmail),
|
|
86
|
+
(e.subject || '').slice(0, 35),
|
|
87
|
+
colorStatus(e.status),
|
|
88
|
+
e.createdAt ? chalk.dim(new Date(e.createdAt).toLocaleString()) : '—',
|
|
89
|
+
])
|
|
90
|
+
);
|
|
91
|
+
info(`Showing last 20 emails. Use ${chalk.cyan('sendcraft emails list --limit 50')} for more.`);
|
|
92
|
+
} catch (e) { sp.fail(chalk.red('Failed')); error(e.message); }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (action === 'stats') {
|
|
96
|
+
const sp = spinner('Loading stats…').start();
|
|
97
|
+
try {
|
|
98
|
+
const data = await client.get('/emails/stats/summary');
|
|
99
|
+
sp.succeed(chalk.dim('Loaded'));
|
|
100
|
+
const s = data.stats || data;
|
|
101
|
+
table(
|
|
102
|
+
['Metric', 'Value'],
|
|
103
|
+
[
|
|
104
|
+
['Total Sent', chalk.bold(String(s.totalSent ?? '—'))],
|
|
105
|
+
['Delivered', chalk.green(String(s.delivered ?? '—'))],
|
|
106
|
+
['Opened', chalk.cyan(String(s.opened ?? '—'))],
|
|
107
|
+
['Clicked', chalk.cyan(String(s.clicked ?? '—'))],
|
|
108
|
+
['Bounced', chalk.red(String(s.bounced ?? '—'))],
|
|
109
|
+
['Open Rate', s.openRate != null ? chalk.green(`${(s.openRate * 100).toFixed(1)}%`) : '—'],
|
|
110
|
+
['Click Rate', s.clickRate != null ? chalk.cyan( `${(s.clickRate * 100).toFixed(1)}%`) : '—'],
|
|
111
|
+
]
|
|
112
|
+
);
|
|
113
|
+
} catch (e) { sp.fail(chalk.red('Failed')); error(e.message); }
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function screenCampaigns() {
|
|
118
|
+
const sp = spinner('Fetching campaigns…').start();
|
|
119
|
+
try {
|
|
120
|
+
const data = await client.get('/campaigns');
|
|
121
|
+
sp.succeed(chalk.dim('Loaded'));
|
|
122
|
+
const campaigns = data.campaigns || [];
|
|
123
|
+
if (!campaigns.length) return info('No campaigns found.');
|
|
124
|
+
table(
|
|
125
|
+
['ID', 'Name', 'Status', 'Recipients', 'Created'],
|
|
126
|
+
campaigns.map(c => [
|
|
127
|
+
chalk.dim(String(c._id).slice(-8)),
|
|
128
|
+
chalk.bold((c.name || '').slice(0, 30)),
|
|
129
|
+
colorStatus(c.status),
|
|
130
|
+
c.recipientCount != null ? chalk.cyan(String(c.recipientCount)) : '—',
|
|
131
|
+
c.createdAt ? chalk.dim(new Date(c.createdAt).toLocaleDateString()) : '—',
|
|
132
|
+
])
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const { campaignId } = await prompts({
|
|
136
|
+
type: 'text',
|
|
137
|
+
name: 'campaignId',
|
|
138
|
+
message: 'Enter campaign ID to send (or leave blank to go back)',
|
|
139
|
+
});
|
|
140
|
+
if (!campaignId) return;
|
|
141
|
+
|
|
142
|
+
const { confirm } = await prompts({
|
|
143
|
+
type: 'confirm',
|
|
144
|
+
name: 'confirm',
|
|
145
|
+
message: `Send campaign ${chalk.cyan(campaignId)}?`,
|
|
146
|
+
initial: false,
|
|
147
|
+
});
|
|
148
|
+
if (!confirm) return;
|
|
149
|
+
|
|
150
|
+
const sp2 = spinner('Sending campaign…').start();
|
|
151
|
+
await client.post(`/campaigns/${campaignId}/send`, {});
|
|
152
|
+
sp2.succeed(chalk.dim('Done'));
|
|
153
|
+
success('Campaign sent!');
|
|
154
|
+
} catch (e) { error(e.message); }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function screenSubscribers() {
|
|
158
|
+
const sp = spinner('Fetching subscribers…').start();
|
|
159
|
+
try {
|
|
160
|
+
const data = await client.get('/subscribers?page=1&limit=20');
|
|
161
|
+
sp.succeed(chalk.dim('Loaded'));
|
|
162
|
+
const subs = data.subscribers || [];
|
|
163
|
+
if (!subs.length) return info('No subscribers found.');
|
|
164
|
+
table(
|
|
165
|
+
['Email', 'Name', 'Status', 'Joined'],
|
|
166
|
+
subs.map(s => [
|
|
167
|
+
chalk.cyan(s.email),
|
|
168
|
+
[s.firstName, s.lastName].filter(Boolean).join(' ') || chalk.dim('—'),
|
|
169
|
+
colorStatus(s.status),
|
|
170
|
+
s.createdAt ? chalk.dim(new Date(s.createdAt).toLocaleDateString()) : '—',
|
|
171
|
+
])
|
|
172
|
+
);
|
|
173
|
+
info(`Showing first 20. Use ${chalk.cyan('sendcraft subscribers list')} for more options.`);
|
|
174
|
+
} catch (e) { error(e.message); }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function screenDomains() {
|
|
178
|
+
const sp = spinner('Fetching domains…').start();
|
|
179
|
+
try {
|
|
180
|
+
const data = await client.get('/domains');
|
|
181
|
+
sp.succeed(chalk.dim('Loaded'));
|
|
182
|
+
const domains = data.domains || [];
|
|
183
|
+
if (!domains.length) {
|
|
184
|
+
info('No domains added yet.');
|
|
185
|
+
const { add } = await prompts({ type: 'confirm', name: 'add', message: 'Add a domain now?', initial: true });
|
|
186
|
+
if (!add) return;
|
|
187
|
+
} else {
|
|
188
|
+
table(
|
|
189
|
+
['Domain', 'Status', 'SPF', 'DKIM', 'DMARC'],
|
|
190
|
+
domains.map(d => [
|
|
191
|
+
chalk.bold(d.domain),
|
|
192
|
+
colorStatus(d.status),
|
|
193
|
+
d.spfVerified ? chalk.green('✓') : chalk.red('✗'),
|
|
194
|
+
d.dkimVerified ? chalk.green('✓') : chalk.red('✗'),
|
|
195
|
+
d.dmarcVerified? chalk.green('✓') : chalk.red('✗'),
|
|
196
|
+
])
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const { action } = await prompts({
|
|
201
|
+
type: 'select',
|
|
202
|
+
name: 'action',
|
|
203
|
+
message: 'Domains',
|
|
204
|
+
choices: [
|
|
205
|
+
{ title: 'Add a domain', value: 'add' },
|
|
206
|
+
{ title: 'Verify a domain', value: 'verify' },
|
|
207
|
+
{ title: chalk.dim('← Back'), value: 'back' },
|
|
208
|
+
],
|
|
209
|
+
});
|
|
210
|
+
if (!action || action === 'back') return;
|
|
211
|
+
|
|
212
|
+
if (action === 'add') {
|
|
213
|
+
const { domain } = await prompts({ type: 'text', name: 'domain', message: 'Domain (e.g. mystore.com)' });
|
|
214
|
+
if (!domain) return;
|
|
215
|
+
const sp2 = spinner(`Adding ${chalk.cyan(domain)}…`).start();
|
|
216
|
+
const result = await client.post('/domains', { domain });
|
|
217
|
+
sp2.succeed(chalk.dim('Added'));
|
|
218
|
+
success(`${domain} added.`);
|
|
219
|
+
console.log('\n' + sendcraftColor(' ─── Add these DNS records ───') + '\n');
|
|
220
|
+
(result.dnsRecords || []).forEach(r => {
|
|
221
|
+
console.log(` ${chalk.bold.cyan(r.purpose)}${r.optional ? chalk.dim(' (optional)') : ''}`);
|
|
222
|
+
console.log(` Name: ${r.name}`);
|
|
223
|
+
console.log(` Value: ${r.value}\n`);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (action === 'verify') {
|
|
228
|
+
const { id } = await prompts({ type: 'text', name: 'id', message: 'Domain ID' });
|
|
229
|
+
if (!id) return;
|
|
230
|
+
const sp2 = spinner('Checking DNS…').start();
|
|
231
|
+
const result = await client.post(`/domains/${id}/verify`);
|
|
232
|
+
result.verified ? sp2.succeed(gradient(['#10b981', '#3b82f6'])('All verified!')) : sp2.warn(chalk.yellow('Still pending'));
|
|
233
|
+
const r = result.results || {};
|
|
234
|
+
table(['Record', 'Status'], [
|
|
235
|
+
['SPF', r.spf ? chalk.green('✓') : chalk.red('✗')],
|
|
236
|
+
['DKIM', r.dkim ? chalk.green('✓') : chalk.red('✗')],
|
|
237
|
+
['DMARC', r.dmarc ? chalk.green('✓') : chalk.red('✗')],
|
|
238
|
+
]);
|
|
239
|
+
}
|
|
240
|
+
} catch (e) { error(e.message); }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function screenKeys() {
|
|
244
|
+
const sp = spinner('Fetching API keys…').start();
|
|
245
|
+
try {
|
|
246
|
+
const data = await client.get('/user/keys');
|
|
247
|
+
sp.succeed(chalk.dim('Loaded'));
|
|
248
|
+
const keys = data.keys || [];
|
|
249
|
+
if (keys.length) {
|
|
250
|
+
table(
|
|
251
|
+
['Name', 'Key', 'Permissions', 'Last Used'],
|
|
252
|
+
keys.map(k => [
|
|
253
|
+
chalk.bold(k.name),
|
|
254
|
+
chalk.dim(k.maskedKey || '***'),
|
|
255
|
+
k.permissions === 'full_access' ? chalk.green('full') : chalk.yellow('sending'),
|
|
256
|
+
k.lastUsedAt ? chalk.dim(new Date(k.lastUsedAt).toLocaleDateString()) : chalk.dim('Never'),
|
|
257
|
+
])
|
|
258
|
+
);
|
|
259
|
+
} else {
|
|
260
|
+
info('No API keys yet.');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const { action } = await prompts({
|
|
264
|
+
type: 'select',
|
|
265
|
+
name: 'action',
|
|
266
|
+
message: 'API Keys',
|
|
267
|
+
choices: [
|
|
268
|
+
{ title: 'Create a new key', value: 'create' },
|
|
269
|
+
{ title: chalk.dim('← Back'), value: 'back' },
|
|
270
|
+
],
|
|
271
|
+
});
|
|
272
|
+
if (!action || action === 'back') return;
|
|
273
|
+
|
|
274
|
+
const { name, permissions } = await prompts([
|
|
275
|
+
{ type: 'text', name: 'name', message: 'Key name' },
|
|
276
|
+
{ type: 'select', name: 'permissions', message: 'Permissions',
|
|
277
|
+
choices: [
|
|
278
|
+
{ title: 'full_access — can do everything', value: 'full_access' },
|
|
279
|
+
{ title: 'sending_access — email send only', value: 'sending_access' },
|
|
280
|
+
]
|
|
281
|
+
},
|
|
282
|
+
]);
|
|
283
|
+
if (!name) return;
|
|
284
|
+
|
|
285
|
+
const sp2 = spinner(`Creating "${chalk.cyan(name)}"…`).start();
|
|
286
|
+
const result = await client.post('/user/keys', { name, permissions });
|
|
287
|
+
sp2.succeed(chalk.dim('Created'));
|
|
288
|
+
const key = result.key;
|
|
289
|
+
console.log(`\n ${chalk.bold.yellow('API Key (save this — shown once):')}`);
|
|
290
|
+
console.log(` ${chalk.bgBlack.green.bold(' ' + key.key + ' ')}\n`);
|
|
291
|
+
success('Key created. Store it securely!');
|
|
292
|
+
} catch (e) { error(e.message); }
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function screenWarmup() {
|
|
296
|
+
const sp = spinner('Checking warmup…').start();
|
|
297
|
+
try {
|
|
298
|
+
const data = await client.get('/smtp/warmup');
|
|
299
|
+
sp.succeed(chalk.dim('Loaded'));
|
|
300
|
+
if (data.isWarmedUp) {
|
|
301
|
+
console.log('\n ' + gradient(['#10b981', '#3b82f6']).bold('✓ IP fully warmed up — no daily limits!') + '\n');
|
|
302
|
+
} else {
|
|
303
|
+
const ratio = data.dailyLimit ? Math.min(data.todayCount / data.dailyLimit, 1) : 0;
|
|
304
|
+
const filled = Math.round(ratio * 24);
|
|
305
|
+
const bar = chalk.dim('[') + chalk.green('█'.repeat(filled)) + chalk.dim('░'.repeat(24 - filled)) + chalk.dim(']');
|
|
306
|
+
console.log(`\n ${chalk.bold.magenta('Day ' + data.warmupDay)} ${bar} ${chalk.cyan(data.todayCount)}${chalk.dim('/' + data.dailyLimit)}\n`);
|
|
307
|
+
}
|
|
308
|
+
table(['Field', 'Value'], [
|
|
309
|
+
['Warmup Day', chalk.bold(String(data.warmupDay))],
|
|
310
|
+
['Daily Limit', data.isWarmedUp ? chalk.green('Unlimited') : chalk.yellow(String(data.dailyLimit))],
|
|
311
|
+
['Sent Today', chalk.cyan(String(data.todayCount))],
|
|
312
|
+
['Remaining', data.isWarmedUp ? '∞' : String(data.remainingToday)],
|
|
313
|
+
]);
|
|
314
|
+
} catch (e) { error(e.message); }
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function screenMcp() {
|
|
318
|
+
const apiKey = process.env.SENDCRAFT_API_KEY || '<your-api-key>';
|
|
319
|
+
console.log('\n' + sendcraftColor(' ✦ SendCraft MCP Server') + '\n');
|
|
320
|
+
console.log(chalk.bold(' Install:'));
|
|
321
|
+
console.log(` ${chalk.cyan('npm install -g sendcraft-mcp')}\n`);
|
|
322
|
+
console.log(chalk.bold(' Claude Desktop config:'));
|
|
323
|
+
console.log(chalk.dim(' ~/Library/Application Support/Claude/claude_desktop_config.json\n'));
|
|
324
|
+
const cfg = { mcpServers: { sendcraft: { command: 'npx', args: ['sendcraft-mcp'], env: { SENDCRAFT_API_KEY: apiKey } } } };
|
|
325
|
+
console.log(' ' + chalk.bgBlack(JSON.stringify(cfg, null, 2).split('\n').join('\n ')) + '\n');
|
|
326
|
+
info(`Docs: https://sendcraft.online/docs/mcp`);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function screenConfig() {
|
|
330
|
+
const { action } = await prompts({
|
|
331
|
+
type: 'select',
|
|
332
|
+
name: 'action',
|
|
333
|
+
message: 'Config',
|
|
334
|
+
choices: [
|
|
335
|
+
{ title: 'Set API key', value: 'key' },
|
|
336
|
+
{ title: 'Show config', value: 'show' },
|
|
337
|
+
{ title: chalk.dim('← Back'), value: 'back' },
|
|
338
|
+
],
|
|
339
|
+
});
|
|
340
|
+
if (!action || action === 'back') return;
|
|
341
|
+
|
|
342
|
+
if (action === 'key') {
|
|
343
|
+
const { key } = await prompts({ type: 'password', name: 'key', message: 'Paste your API key' });
|
|
344
|
+
if (!key) return;
|
|
345
|
+
const cfg = require('./config').load();
|
|
346
|
+
cfg.api_key = key;
|
|
347
|
+
require('./config').save(cfg);
|
|
348
|
+
success('API key saved.');
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (action === 'show') {
|
|
352
|
+
const { getApiKey, getBaseUrl, CONFIG_FILE } = require('./config');
|
|
353
|
+
const k = getApiKey();
|
|
354
|
+
if (!k) return error('No key set. Choose "Set API key".');
|
|
355
|
+
const masked = k.length > 12 ? k.slice(0, 8) + '…' + k.slice(-4) : '***';
|
|
356
|
+
console.log(`\n ${chalk.bold('Key:')} ${chalk.green(masked)}`);
|
|
357
|
+
console.log(` ${chalk.bold('URL:')} ${chalk.cyan(getBaseUrl())}`);
|
|
358
|
+
console.log(` ${chalk.bold('File:')} ${chalk.dim(CONFIG_FILE)}\n`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ─── Main loop ────────────────────────────────────────────────────────────────
|
|
363
|
+
|
|
364
|
+
module.exports = async function interactive() {
|
|
365
|
+
banner(pkg.version);
|
|
366
|
+
|
|
367
|
+
// Check for API key
|
|
368
|
+
const { getApiKey } = require('./config');
|
|
369
|
+
if (!getApiKey()) {
|
|
370
|
+
console.log(chalk.yellow(' ⚠ No API key configured yet.\n'));
|
|
371
|
+
const { setup } = await prompts({
|
|
372
|
+
type: 'confirm', name: 'setup',
|
|
373
|
+
message: 'Log in to SendCraft now?',
|
|
374
|
+
initial: true,
|
|
375
|
+
});
|
|
376
|
+
if (setup) await require('./commands/login').runInteractive();
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
while (true) {
|
|
380
|
+
console.log();
|
|
381
|
+
const { choice } = await prompts({
|
|
382
|
+
type: 'select',
|
|
383
|
+
name: 'choice',
|
|
384
|
+
message: sendcraftGradient('What would you like to do?'),
|
|
385
|
+
choices: MAIN_MENU,
|
|
386
|
+
hint: 'Use arrow keys + Enter',
|
|
387
|
+
}, { onCancel: () => process.exit(0) });
|
|
388
|
+
|
|
389
|
+
if (!choice || choice === 'exit') {
|
|
390
|
+
console.log('\n ' + chalk.dim('Bye! 👋') + '\n');
|
|
391
|
+
process.exit(0);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
console.log();
|
|
395
|
+
try {
|
|
396
|
+
if (choice === 'send') await screenSendEmail();
|
|
397
|
+
if (choice === 'emails') await screenEmails();
|
|
398
|
+
if (choice === 'campaigns') await screenCampaigns();
|
|
399
|
+
if (choice === 'subscribers') await screenSubscribers();
|
|
400
|
+
if (choice === 'domains') await screenDomains();
|
|
401
|
+
if (choice === 'keys') await screenKeys();
|
|
402
|
+
if (choice === 'warmup') await screenWarmup();
|
|
403
|
+
if (choice === 'mcp') await screenMcp();
|
|
404
|
+
if (choice === 'login') await require('./commands/login').runInteractive();
|
|
405
|
+
if (choice === 'config') await screenConfig();
|
|
406
|
+
} catch (e) {
|
|
407
|
+
error('Unexpected error: ' + e.message);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
};
|
package/lib/output.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
const chalk = require('chalk');
|
|
5
5
|
const Table = require('cli-table3');
|
|
6
|
+
const ora = require('ora');
|
|
6
7
|
|
|
7
8
|
const STATUS_COLORS = {
|
|
8
9
|
delivered: 'green',
|
|
@@ -28,7 +29,10 @@ function colorStatus(status) {
|
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
function table(headers, rows) {
|
|
31
|
-
const t = new Table({
|
|
32
|
+
const t = new Table({
|
|
33
|
+
head: headers.map(h => chalk.bold.cyan(h)),
|
|
34
|
+
style: { compact: false, border: ['dim'] },
|
|
35
|
+
});
|
|
32
36
|
rows.forEach(row => t.push(row));
|
|
33
37
|
console.log(t.toString());
|
|
34
38
|
}
|
|
@@ -37,20 +41,38 @@ function json(data) {
|
|
|
37
41
|
console.log(JSON.stringify(data, null, 2));
|
|
38
42
|
}
|
|
39
43
|
|
|
44
|
+
// Use gradient helpers from banner if available, fallback to plain chalk
|
|
40
45
|
function success(msg) {
|
|
41
|
-
|
|
46
|
+
try {
|
|
47
|
+
const { printSuccess } = require('./banner');
|
|
48
|
+
printSuccess(msg);
|
|
49
|
+
} catch { console.log(chalk.green(' ✓ ') + chalk.bold(msg)); }
|
|
42
50
|
}
|
|
43
51
|
|
|
44
52
|
function error(msg) {
|
|
45
|
-
|
|
53
|
+
try {
|
|
54
|
+
const { printError } = require('./banner');
|
|
55
|
+
printError(msg);
|
|
56
|
+
} catch { console.error(chalk.red(' ✗ ') + msg); }
|
|
46
57
|
}
|
|
47
58
|
|
|
48
59
|
function info(msg) {
|
|
49
|
-
|
|
60
|
+
try {
|
|
61
|
+
const { printInfo } = require('./banner');
|
|
62
|
+
printInfo(msg);
|
|
63
|
+
} catch { console.log(chalk.blue(' ℹ ') + msg); }
|
|
50
64
|
}
|
|
51
65
|
|
|
52
66
|
function warn(msg) {
|
|
53
|
-
|
|
67
|
+
try {
|
|
68
|
+
const { printWarn } = require('./banner');
|
|
69
|
+
printWarn(msg);
|
|
70
|
+
} catch { console.log(chalk.yellow(' ⚠ ') + msg); }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Returns an ora spinner with a consistent style */
|
|
74
|
+
function spinner(text) {
|
|
75
|
+
return ora({ text, spinner: 'dots', color: 'magenta' });
|
|
54
76
|
}
|
|
55
77
|
|
|
56
78
|
function printOrJson(data, asJson, printFn) {
|
|
@@ -58,4 +80,4 @@ function printOrJson(data, asJson, printFn) {
|
|
|
58
80
|
printFn(data);
|
|
59
81
|
}
|
|
60
82
|
|
|
61
|
-
module.exports = { table, json, success, error, info, warn, printOrJson, colorStatus };
|
|
83
|
+
module.exports = { table, json, success, error, info, warn, spinner, printOrJson, colorStatus };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sendcraft-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Official SendCraft CLI — send emails, manage campaigns, and more from your terminal",
|
|
5
5
|
"bin": {
|
|
6
6
|
"sendcraft": "./bin/sendcraft.js"
|
|
@@ -9,13 +9,21 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"test": "echo \"No tests yet\""
|
|
11
11
|
},
|
|
12
|
-
"keywords": [
|
|
12
|
+
"keywords": [
|
|
13
|
+
"sendcraft",
|
|
14
|
+
"email",
|
|
15
|
+
"cli",
|
|
16
|
+
"transactional"
|
|
17
|
+
],
|
|
13
18
|
"author": "SendCraft Team <sendcraft.team@gmail.com>",
|
|
14
19
|
"license": "MIT",
|
|
15
20
|
"dependencies": {
|
|
21
|
+
"boxen": "^8.0.1",
|
|
16
22
|
"chalk": "^4.1.2",
|
|
17
23
|
"cli-table3": "^0.6.5",
|
|
18
24
|
"commander": "^11.1.0",
|
|
25
|
+
"figlet": "^1.11.0",
|
|
26
|
+
"gradient-string": "^3.0.0",
|
|
19
27
|
"ora": "^5.4.1",
|
|
20
28
|
"prompts": "^2.4.2"
|
|
21
29
|
},
|