sendcraft-cli 1.0.0 → 1.1.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/bin/sendcraft.js +7 -8
- package/lib/banner.js +53 -0
- 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/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/interactive.js +408 -0
- package/lib/output.js +28 -6
- package/package.json +10 -2
package/bin/sendcraft.js
CHANGED
|
@@ -6,7 +6,6 @@ const { program } = require('commander');
|
|
|
6
6
|
const chalk = require('chalk');
|
|
7
7
|
const pkg = require('../package.json');
|
|
8
8
|
|
|
9
|
-
// Sub-commands
|
|
10
9
|
program
|
|
11
10
|
.name('sendcraft')
|
|
12
11
|
.description('Official SendCraft CLI')
|
|
@@ -20,17 +19,17 @@ program.addCommand(require('../lib/commands/subscribers'));
|
|
|
20
19
|
program.addCommand(require('../lib/commands/domains'));
|
|
21
20
|
program.addCommand(require('../lib/commands/keys'));
|
|
22
21
|
program.addCommand(require('../lib/commands/warmup'));
|
|
22
|
+
program.addCommand(require('../lib/commands/mcp'));
|
|
23
23
|
|
|
24
|
-
// Friendly error on unknown command
|
|
25
24
|
program.on('command:*', (args) => {
|
|
26
|
-
console.error(chalk.red(`Unknown command: ${args[0]}`));
|
|
27
|
-
console.error('Run ' + chalk.bold('sendcraft --help') + '
|
|
25
|
+
console.error(chalk.red(` ✗ Unknown command: ${args[0]}`));
|
|
26
|
+
console.error(' Run ' + chalk.bold('sendcraft --help') + ' for available commands.');
|
|
28
27
|
process.exit(1);
|
|
29
28
|
});
|
|
30
29
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
// Show help if called with no args
|
|
30
|
+
// Interactive mode when called with no args
|
|
34
31
|
if (process.argv.length < 3) {
|
|
35
|
-
|
|
32
|
+
require('../lib/interactive')();
|
|
33
|
+
} else {
|
|
34
|
+
program.parse(process.argv);
|
|
36
35
|
}
|
package/lib/banner.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Animated banner shown on `sendcraft` with no args.
|
|
3
|
+
*/
|
|
4
|
+
const figlet = require('figlet');
|
|
5
|
+
const _grad = require('gradient-string'); const gradient = _grad.default || _grad;
|
|
6
|
+
const _boxen = require('boxen'); const boxen = _boxen.default || _boxen;
|
|
7
|
+
const chalk = require('chalk');
|
|
8
|
+
|
|
9
|
+
const sendcraftGradient = gradient(['#6366f1', '#8b5cf6', '#ec4899']);
|
|
10
|
+
const successGradient = gradient(['#10b981', '#3b82f6']);
|
|
11
|
+
|
|
12
|
+
function banner(version) {
|
|
13
|
+
const text = figlet.textSync('SendCraft', {
|
|
14
|
+
font: 'Big',
|
|
15
|
+
horizontalLayout: 'default',
|
|
16
|
+
verticalLayout: 'default',
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
console.log(sendcraftGradient.multiline(text));
|
|
20
|
+
|
|
21
|
+
const box = boxen(
|
|
22
|
+
`${chalk.bold('SendCraft CLI')} ${chalk.dim('v' + version)}\n` +
|
|
23
|
+
`${chalk.dim('Official CLI for the SendCraft email platform')}\n\n` +
|
|
24
|
+
`${chalk.cyan('sendcraft config init')} ${chalk.dim('→ Setup your API key')}\n` +
|
|
25
|
+
`${chalk.cyan('sendcraft send')} ${chalk.dim('→ Send an email')}\n` +
|
|
26
|
+
`${chalk.cyan('sendcraft --help')} ${chalk.dim('→ All commands')}`,
|
|
27
|
+
{
|
|
28
|
+
padding: 1,
|
|
29
|
+
margin: { top: 0, bottom: 1, left: 0, right: 0 },
|
|
30
|
+
borderStyle: 'round',
|
|
31
|
+
borderColor: 'magenta',
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
console.log(box);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function printSuccess(msg) {
|
|
38
|
+
console.log(successGradient(' ✓ ') + chalk.bold(msg));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function printError(msg) {
|
|
42
|
+
console.error(chalk.red(' ✗ ') + msg);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function printInfo(msg) {
|
|
46
|
+
console.log(chalk.blue(' ℹ ') + msg);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function printWarn(msg) {
|
|
50
|
+
console.log(chalk.yellow(' ⚠ ') + msg);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = { banner, printSuccess, printError, printInfo, printWarn, sendcraftGradient, successGradient };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const { Command } = require('commander');
|
|
2
|
-
const
|
|
2
|
+
const chalk = require('chalk');
|
|
3
3
|
const client = require('../client');
|
|
4
|
-
const { table, json, colorStatus, info, success, error } = require('../output');
|
|
4
|
+
const { table, json, colorStatus, info, success, error, spinner } = require('../output');
|
|
5
5
|
|
|
6
6
|
const cmd = new Command('campaigns').description('Manage campaigns');
|
|
7
7
|
|
|
@@ -10,26 +10,26 @@ cmd
|
|
|
10
10
|
.description('List campaigns')
|
|
11
11
|
.option('--json', 'Output raw JSON')
|
|
12
12
|
.action(async (opts) => {
|
|
13
|
-
const
|
|
13
|
+
const sp = spinner('Fetching campaigns…').start();
|
|
14
14
|
try {
|
|
15
15
|
const data = await client.get('/campaigns');
|
|
16
|
-
|
|
16
|
+
sp.succeed(chalk.dim('Loaded'));
|
|
17
17
|
if (opts.json) return json(data);
|
|
18
18
|
const campaigns = data.campaigns || [];
|
|
19
|
-
if (!campaigns.length) return info('No campaigns
|
|
19
|
+
if (!campaigns.length) return info('No campaigns yet. Create one in the dashboard.');
|
|
20
20
|
table(
|
|
21
21
|
['ID', 'Name', 'Subject', 'Status', 'Recipients', 'Created'],
|
|
22
22
|
campaigns.map(c => [
|
|
23
|
-
String(c._id).slice(-8),
|
|
24
|
-
(c.name || '').slice(0, 25),
|
|
23
|
+
chalk.dim(String(c._id).slice(-8)),
|
|
24
|
+
chalk.bold((c.name || '').slice(0, 25)),
|
|
25
25
|
(c.subject || '').slice(0, 30),
|
|
26
26
|
colorStatus(c.status),
|
|
27
|
-
c.recipientCount
|
|
28
|
-
c.createdAt ? new Date(c.createdAt).toLocaleString() : '—',
|
|
27
|
+
c.recipientCount != null ? chalk.cyan(String(c.recipientCount)) : '—',
|
|
28
|
+
c.createdAt ? chalk.dim(new Date(c.createdAt).toLocaleString()) : '—',
|
|
29
29
|
])
|
|
30
30
|
);
|
|
31
31
|
} catch (err) {
|
|
32
|
-
|
|
32
|
+
sp.fail(chalk.red('Failed'));
|
|
33
33
|
error(err.message);
|
|
34
34
|
process.exit(1);
|
|
35
35
|
}
|
|
@@ -38,18 +38,19 @@ cmd
|
|
|
38
38
|
cmd
|
|
39
39
|
.command('send <campaignId>')
|
|
40
40
|
.description('Send or schedule a campaign')
|
|
41
|
-
.option('--schedule <isoDate>', 'Schedule for a specific
|
|
41
|
+
.option('--schedule <isoDate>', 'Schedule for a specific ISO 8601 datetime')
|
|
42
42
|
.option('--json', 'Output raw JSON')
|
|
43
43
|
.action(async (campaignId, opts) => {
|
|
44
|
-
const
|
|
44
|
+
const label = opts.schedule ? `Scheduling campaign…` : `Sending campaign…`;
|
|
45
|
+
const sp = spinner(label).start();
|
|
45
46
|
try {
|
|
46
47
|
const body = opts.schedule ? { scheduledAt: opts.schedule } : {};
|
|
47
48
|
const data = await client.post(`/campaigns/${campaignId}/send`, body);
|
|
48
|
-
|
|
49
|
+
sp.succeed(chalk.dim('Done'));
|
|
49
50
|
if (opts.json) return json(data);
|
|
50
|
-
success(`Campaign ${
|
|
51
|
+
success(`Campaign ${opts.schedule ? 'scheduled for ' + chalk.cyan(opts.schedule) : 'sent!'}`);
|
|
51
52
|
} catch (err) {
|
|
52
|
-
|
|
53
|
+
sp.fail(chalk.red('Failed'));
|
|
53
54
|
error(err.message);
|
|
54
55
|
process.exit(1);
|
|
55
56
|
}
|
package/lib/commands/config.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
const { Command } = require('commander');
|
|
2
2
|
const prompts = require('prompts');
|
|
3
3
|
const chalk = require('chalk');
|
|
4
|
+
const ora = require('ora');
|
|
4
5
|
const { load, save, getApiKey, getBaseUrl, CONFIG_FILE } = require('../config');
|
|
5
|
-
const { success,
|
|
6
|
+
const { success, error, info } = require('../output');
|
|
7
|
+
const { sendcraftGradient } = require('../banner');
|
|
6
8
|
|
|
7
9
|
const cmd = new Command('config')
|
|
8
10
|
.description('Configure your SendCraft credentials');
|
|
@@ -11,20 +13,22 @@ cmd
|
|
|
11
13
|
.command('set-key <apiKey>')
|
|
12
14
|
.description('Save your API key')
|
|
13
15
|
.action((apiKey) => {
|
|
16
|
+
const sp = ora({ text: 'Saving…', spinner: 'dots', color: 'cyan' }).start();
|
|
14
17
|
const cfg = load();
|
|
15
18
|
cfg.api_key = apiKey;
|
|
16
19
|
save(cfg);
|
|
17
|
-
|
|
20
|
+
sp.succeed(chalk.dim('Saved'));
|
|
21
|
+
success(`API key stored at ${chalk.dim(CONFIG_FILE)}`);
|
|
18
22
|
});
|
|
19
23
|
|
|
20
24
|
cmd
|
|
21
25
|
.command('set-url <url>')
|
|
22
|
-
.description('Override the API base URL
|
|
26
|
+
.description('Override the API base URL')
|
|
23
27
|
.action((url) => {
|
|
24
28
|
const cfg = load();
|
|
25
29
|
cfg.base_url = url;
|
|
26
30
|
save(cfg);
|
|
27
|
-
success(`Base URL
|
|
31
|
+
success(`Base URL updated: ${chalk.cyan(url)}`);
|
|
28
32
|
});
|
|
29
33
|
|
|
30
34
|
cmd
|
|
@@ -34,26 +38,28 @@ cmd
|
|
|
34
38
|
const key = getApiKey();
|
|
35
39
|
const url = getBaseUrl();
|
|
36
40
|
if (!key) {
|
|
37
|
-
error('No API key set. Run: sendcraft config set-key <key>');
|
|
38
|
-
info('Or set SENDCRAFT_API_KEY
|
|
41
|
+
error('No API key set. Run: ' + chalk.cyan('sendcraft config set-key <key>'));
|
|
42
|
+
info('Or set the ' + chalk.bold('SENDCRAFT_API_KEY') + ' environment variable.');
|
|
39
43
|
return;
|
|
40
44
|
}
|
|
41
45
|
const masked = key.length > 12 ? key.slice(0, 8) + '…' + key.slice(-4) : '***';
|
|
42
|
-
console.log(chalk.bold('API Key: ') + masked);
|
|
43
|
-
console.log(chalk.bold('Base URL: ') + url);
|
|
44
|
-
console.log(chalk.bold('Config: ') + CONFIG_FILE);
|
|
46
|
+
console.log('\n' + chalk.bold(' API Key: ') + chalk.green(masked));
|
|
47
|
+
console.log(chalk.bold(' Base URL: ') + chalk.cyan(url));
|
|
48
|
+
console.log(chalk.bold(' Config: ') + chalk.dim(CONFIG_FILE) + '\n');
|
|
45
49
|
});
|
|
46
50
|
|
|
47
51
|
cmd
|
|
48
52
|
.command('init')
|
|
49
|
-
.description('Interactive setup')
|
|
53
|
+
.description('Interactive first-time setup')
|
|
50
54
|
.action(async () => {
|
|
55
|
+
console.log('\n' + sendcraftGradient(' ✦ SendCraft Setup\n'));
|
|
56
|
+
|
|
51
57
|
const answers = await prompts([
|
|
52
58
|
{
|
|
53
59
|
type: 'password',
|
|
54
60
|
name: 'api_key',
|
|
55
|
-
message: 'Paste your
|
|
56
|
-
validate: v => v.
|
|
61
|
+
message: 'Paste your API key',
|
|
62
|
+
validate: v => (v && v.length > 10) ? true : 'Enter a valid API key',
|
|
57
63
|
},
|
|
58
64
|
{
|
|
59
65
|
type: 'text',
|
|
@@ -61,10 +67,12 @@ cmd
|
|
|
61
67
|
message: 'API base URL',
|
|
62
68
|
initial: 'https://api.sendcraft.online/api',
|
|
63
69
|
},
|
|
64
|
-
]);
|
|
65
|
-
|
|
70
|
+
], { onCancel: () => { error('Cancelled.'); process.exit(0); } });
|
|
71
|
+
|
|
72
|
+
const sp = ora({ text: 'Saving config…', spinner: 'dots2', color: 'cyan' }).start();
|
|
66
73
|
save(answers);
|
|
67
|
-
|
|
74
|
+
sp.succeed(chalk.dim('Config saved'));
|
|
75
|
+
success(`Ready! Run ${chalk.cyan('sendcraft send --help')} to get started.`);
|
|
68
76
|
});
|
|
69
77
|
|
|
70
78
|
module.exports = cmd;
|
package/lib/commands/domains.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
const { Command } = require('commander');
|
|
2
|
-
const ora = require('ora');
|
|
3
2
|
const chalk = require('chalk');
|
|
3
|
+
const _grad = require('gradient-string'); const gradient = _grad.default || _grad;
|
|
4
4
|
const client = require('../client');
|
|
5
|
-
const { table, json, colorStatus, info, success, error } = require('../output');
|
|
5
|
+
const { table, json, colorStatus, info, success, error, spinner } = require('../output');
|
|
6
6
|
|
|
7
7
|
const cmd = new Command('domains').description('Manage sender domains');
|
|
8
8
|
|
|
@@ -11,26 +11,26 @@ cmd
|
|
|
11
11
|
.description('List all domains')
|
|
12
12
|
.option('--json', 'Output raw JSON')
|
|
13
13
|
.action(async (opts) => {
|
|
14
|
-
const
|
|
14
|
+
const sp = spinner('Fetching domains…').start();
|
|
15
15
|
try {
|
|
16
16
|
const data = await client.get('/domains');
|
|
17
|
-
|
|
17
|
+
sp.succeed(chalk.dim('Loaded'));
|
|
18
18
|
if (opts.json) return json(data);
|
|
19
19
|
const domains = data.domains || [];
|
|
20
|
-
if (!domains.length) return info('No domains added yet.');
|
|
20
|
+
if (!domains.length) return info('No domains added yet. Run: ' + chalk.cyan('sendcraft domains add <domain>'));
|
|
21
21
|
table(
|
|
22
22
|
['Domain', 'Status', 'SPF', 'DKIM', 'DMARC', 'Added'],
|
|
23
23
|
domains.map(d => [
|
|
24
|
-
d.domain,
|
|
24
|
+
chalk.bold(d.domain),
|
|
25
25
|
colorStatus(d.status),
|
|
26
|
-
d.spfVerified
|
|
26
|
+
d.spfVerified ? chalk.green('✓') : chalk.red('✗'),
|
|
27
27
|
d.dkimVerified ? chalk.green('✓') : chalk.red('✗'),
|
|
28
|
-
d.dmarcVerified
|
|
29
|
-
d.createdAt ? new Date(d.createdAt).toLocaleDateString() : '—',
|
|
28
|
+
d.dmarcVerified? chalk.green('✓') : chalk.red('✗'),
|
|
29
|
+
d.createdAt ? chalk.dim(new Date(d.createdAt).toLocaleDateString()) : '—',
|
|
30
30
|
])
|
|
31
31
|
);
|
|
32
32
|
} catch (err) {
|
|
33
|
-
|
|
33
|
+
sp.fail(chalk.red('Failed'));
|
|
34
34
|
error(err.message);
|
|
35
35
|
process.exit(1);
|
|
36
36
|
}
|
|
@@ -41,22 +41,24 @@ cmd
|
|
|
41
41
|
.description('Add a new sender domain')
|
|
42
42
|
.option('--json', 'Output raw JSON')
|
|
43
43
|
.action(async (domain, opts) => {
|
|
44
|
-
const
|
|
44
|
+
const sp = spinner(`Adding ${chalk.cyan(domain)}…`).start();
|
|
45
45
|
try {
|
|
46
46
|
const data = await client.post('/domains', { domain });
|
|
47
|
-
|
|
47
|
+
sp.succeed(chalk.dim('Domain added'));
|
|
48
48
|
if (opts.json) return json(data);
|
|
49
|
-
success(
|
|
50
|
-
|
|
49
|
+
success(`${chalk.bold(domain)} added.`);
|
|
50
|
+
console.log('\n' + gradient(['#6366f1', '#ec4899'])(' ─── DNS Records to Configure ───') + '\n');
|
|
51
51
|
(data.dnsRecords || []).forEach(r => {
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
console.log(`
|
|
55
|
-
console.log(`
|
|
56
|
-
console.log(`
|
|
52
|
+
const verified = r.verified ? chalk.green(' ✓') : '';
|
|
53
|
+
const optional = r.optional ? chalk.dim(' (optional)') : '';
|
|
54
|
+
console.log(` ${chalk.bold.cyan(r.purpose)}${optional}${verified}`);
|
|
55
|
+
console.log(` ${chalk.dim('Type:')} ${r.type}`);
|
|
56
|
+
console.log(` ${chalk.dim('Name:')} ${r.name}`);
|
|
57
|
+
console.log(` ${chalk.dim('Value:')} ${chalk.white(r.value)}\n`);
|
|
57
58
|
});
|
|
59
|
+
info(`Run ${chalk.cyan(`sendcraft domains verify <id>`)} after DNS propagates.`);
|
|
58
60
|
} catch (err) {
|
|
59
|
-
|
|
61
|
+
sp.fail(chalk.red('Failed'));
|
|
60
62
|
error(err.message);
|
|
61
63
|
process.exit(1);
|
|
62
64
|
}
|
|
@@ -67,27 +69,28 @@ cmd
|
|
|
67
69
|
.description('Trigger a DNS verification check')
|
|
68
70
|
.option('--json', 'Output raw JSON')
|
|
69
71
|
.action(async (id, opts) => {
|
|
70
|
-
const
|
|
72
|
+
const sp = spinner('Checking DNS records…').start();
|
|
71
73
|
try {
|
|
72
74
|
const data = await client.post(`/domains/${id}/verify`);
|
|
73
|
-
spinner.stop();
|
|
74
|
-
if (opts.json) return json(data);
|
|
75
75
|
if (data.verified) {
|
|
76
|
-
|
|
76
|
+
sp.succeed(gradient(['#10b981', '#3b82f6'])('All records verified!'));
|
|
77
77
|
} else {
|
|
78
|
-
|
|
79
|
-
table(
|
|
80
|
-
['Record', 'Status'],
|
|
81
|
-
[
|
|
82
|
-
['SPF', r.spf ? chalk.green('✓ Verified') : chalk.red('✗ Not found')],
|
|
83
|
-
['DKIM', r.dkim ? chalk.green('✓ Verified') : chalk.red('✗ Not found')],
|
|
84
|
-
['DMARC', r.dmarc ? chalk.green('✓ Verified') : chalk.red('✗ Not found')],
|
|
85
|
-
]
|
|
86
|
-
);
|
|
87
|
-
info(data.message || 'Some records still pending.');
|
|
78
|
+
sp.warn(chalk.yellow('Some records still pending'));
|
|
88
79
|
}
|
|
80
|
+
if (opts.json) return json(data);
|
|
81
|
+
const r = data.results || {};
|
|
82
|
+
table(
|
|
83
|
+
['Record', 'Status'],
|
|
84
|
+
[
|
|
85
|
+
['SPF', r.spf ? chalk.green('✓ Verified') : chalk.red('✗ Not found')],
|
|
86
|
+
['DKIM', r.dkim ? chalk.green('✓ Verified') : chalk.red('✗ Not found')],
|
|
87
|
+
['DMARC', r.dmarc ? chalk.green('✓ Verified') : chalk.red('✗ Not found')],
|
|
88
|
+
]
|
|
89
|
+
);
|
|
90
|
+
if (data.verified) success('Domain is ready to send email!');
|
|
91
|
+
else info(data.message || 'DNS changes can take up to 48 hours to propagate.');
|
|
89
92
|
} catch (err) {
|
|
90
|
-
|
|
93
|
+
sp.fail(chalk.red('Failed'));
|
|
91
94
|
error(err.message);
|
|
92
95
|
process.exit(1);
|
|
93
96
|
}
|
|
@@ -95,24 +98,25 @@ cmd
|
|
|
95
98
|
|
|
96
99
|
cmd
|
|
97
100
|
.command('records <domainId>')
|
|
98
|
-
.description('Show DNS records to configure
|
|
101
|
+
.description('Show DNS records to configure')
|
|
99
102
|
.option('--json', 'Output raw JSON')
|
|
100
103
|
.action(async (id, opts) => {
|
|
101
|
-
const
|
|
104
|
+
const sp = spinner('Loading DNS records…').start();
|
|
102
105
|
try {
|
|
103
106
|
const data = await client.get(`/domains/${id}`);
|
|
104
|
-
|
|
107
|
+
sp.succeed(chalk.dim('Loaded'));
|
|
105
108
|
if (opts.json) return json(data.dnsRecords);
|
|
109
|
+
console.log('\n' + gradient(['#6366f1', '#ec4899'])(' ─── DNS Records ───') + '\n');
|
|
106
110
|
(data.dnsRecords || []).forEach(r => {
|
|
107
|
-
const status
|
|
108
|
-
const optional = r.optional ? chalk.
|
|
109
|
-
console.log(
|
|
110
|
-
console.log(`
|
|
111
|
-
console.log(`
|
|
112
|
-
console.log(`
|
|
111
|
+
const status = r.verified ? chalk.green('✓ verified') : chalk.yellow('⏳ pending');
|
|
112
|
+
const optional = r.optional ? chalk.dim(' (optional)') : '';
|
|
113
|
+
console.log(` ${chalk.bold.cyan(r.purpose)}${optional} ${status}`);
|
|
114
|
+
console.log(` ${chalk.dim('Type:')} ${r.type}`);
|
|
115
|
+
console.log(` ${chalk.dim('Name:')} ${r.name}`);
|
|
116
|
+
console.log(` ${chalk.dim('Value:')} ${chalk.white(r.value)}\n`);
|
|
113
117
|
});
|
|
114
118
|
} catch (err) {
|
|
115
|
-
|
|
119
|
+
sp.fail(chalk.red('Failed'));
|
|
116
120
|
error(err.message);
|
|
117
121
|
process.exit(1);
|
|
118
122
|
}
|
package/lib/commands/emails.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const { Command } = require('commander');
|
|
2
|
-
const
|
|
2
|
+
const chalk = require('chalk');
|
|
3
3
|
const client = require('../client');
|
|
4
|
-
const { table, json, colorStatus, info } = require('../output');
|
|
4
|
+
const { table, json, colorStatus, info, error, spinner } = require('../output');
|
|
5
5
|
|
|
6
6
|
const cmd = new Command('emails').description('Manage emails');
|
|
7
7
|
|
|
@@ -13,29 +13,29 @@ cmd
|
|
|
13
13
|
.option('--status <s>', 'Filter by status (sent, delivered, failed, scheduled)')
|
|
14
14
|
.option('--json', 'Output raw JSON')
|
|
15
15
|
.action(async (opts) => {
|
|
16
|
-
const
|
|
16
|
+
const sp = spinner('Fetching emails…').start();
|
|
17
17
|
try {
|
|
18
18
|
const params = new URLSearchParams({ page: opts.page, limit: opts.limit });
|
|
19
19
|
if (opts.status) params.set('status', opts.status);
|
|
20
20
|
const data = await client.get(`/emails?${params}`);
|
|
21
|
-
|
|
21
|
+
sp.succeed(chalk.dim('Loaded'));
|
|
22
22
|
if (opts.json) return json(data);
|
|
23
23
|
const emails = data.emails || [];
|
|
24
24
|
if (!emails.length) return info('No emails found.');
|
|
25
25
|
table(
|
|
26
26
|
['ID', 'To', 'Subject', 'Status', 'Sent At'],
|
|
27
27
|
emails.map(e => [
|
|
28
|
-
String(e._id).slice(-8),
|
|
29
|
-
e.toEmail,
|
|
28
|
+
chalk.dim(String(e._id).slice(-8)),
|
|
29
|
+
chalk.cyan(e.toEmail),
|
|
30
30
|
(e.subject || '').slice(0, 40),
|
|
31
31
|
colorStatus(e.status),
|
|
32
|
-
e.createdAt ? new Date(e.createdAt).toLocaleString() : '—',
|
|
32
|
+
e.createdAt ? chalk.dim(new Date(e.createdAt).toLocaleString()) : '—',
|
|
33
33
|
])
|
|
34
34
|
);
|
|
35
|
-
info(`Page ${opts.page} · ${emails.length} of ${data.total
|
|
35
|
+
info(`Page ${opts.page} · ${emails.length} of ${chalk.bold(data.total ?? '?')}`);
|
|
36
36
|
} catch (err) {
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
sp.fail(chalk.red('Failed'));
|
|
38
|
+
error(err.message);
|
|
39
39
|
process.exit(1);
|
|
40
40
|
}
|
|
41
41
|
});
|
|
@@ -45,28 +45,32 @@ cmd
|
|
|
45
45
|
.description('Show email stats summary')
|
|
46
46
|
.option('--json', 'Output raw JSON')
|
|
47
47
|
.action(async (opts) => {
|
|
48
|
-
const
|
|
48
|
+
const sp = spinner('Loading stats…').start();
|
|
49
49
|
try {
|
|
50
50
|
const data = await client.get('/emails/stats/summary');
|
|
51
|
-
|
|
51
|
+
sp.succeed(chalk.dim('Loaded'));
|
|
52
52
|
if (opts.json) return json(data);
|
|
53
53
|
const s = data.stats || data;
|
|
54
|
+
|
|
55
|
+
const openRate = s.openRate != null ? chalk.green(`${(s.openRate * 100).toFixed(1)}%`) : '—';
|
|
56
|
+
const clickRate = s.clickRate != null ? chalk.cyan( `${(s.clickRate * 100).toFixed(1)}%`) : '—';
|
|
57
|
+
|
|
54
58
|
table(
|
|
55
59
|
['Metric', 'Value'],
|
|
56
60
|
[
|
|
57
|
-
['Total Sent',
|
|
58
|
-
['Delivered',
|
|
59
|
-
['Opened',
|
|
60
|
-
['Clicked',
|
|
61
|
-
['Bounced',
|
|
62
|
-
['Failed',
|
|
63
|
-
['Open Rate',
|
|
64
|
-
['Click Rate',
|
|
61
|
+
['Total Sent', chalk.bold(String(s.totalSent ?? '—'))],
|
|
62
|
+
['Delivered', chalk.green(String(s.delivered ?? '—'))],
|
|
63
|
+
['Opened', chalk.cyan(String(s.opened ?? '—'))],
|
|
64
|
+
['Clicked', chalk.cyan(String(s.clicked ?? '—'))],
|
|
65
|
+
['Bounced', chalk.red(String(s.bounced ?? '—'))],
|
|
66
|
+
['Failed', chalk.red(String(s.failed ?? '—'))],
|
|
67
|
+
['Open Rate', openRate],
|
|
68
|
+
['Click Rate', clickRate],
|
|
65
69
|
]
|
|
66
70
|
);
|
|
67
71
|
} catch (err) {
|
|
68
|
-
|
|
69
|
-
|
|
72
|
+
sp.fail(chalk.red('Failed'));
|
|
73
|
+
error(err.message);
|
|
70
74
|
process.exit(1);
|
|
71
75
|
}
|
|
72
76
|
});
|
|
@@ -76,26 +80,26 @@ cmd
|
|
|
76
80
|
.description('Get details for a single email')
|
|
77
81
|
.option('--json', 'Output raw JSON')
|
|
78
82
|
.action(async (emailId, opts) => {
|
|
79
|
-
const
|
|
83
|
+
const sp = spinner(`Fetching email ${chalk.dim(emailId.slice(-8))}…`).start();
|
|
80
84
|
try {
|
|
81
85
|
const data = await client.get(`/emails/${emailId}`);
|
|
82
|
-
|
|
86
|
+
sp.succeed(chalk.dim('Loaded'));
|
|
83
87
|
if (opts.json) return json(data);
|
|
84
88
|
const e = data.email || data;
|
|
85
89
|
table(
|
|
86
90
|
['Field', 'Value'],
|
|
87
91
|
[
|
|
88
|
-
['ID',
|
|
89
|
-
['To',
|
|
90
|
-
['Subject', e.subject],
|
|
91
|
-
['Status',
|
|
92
|
-
['From',
|
|
92
|
+
['ID', chalk.dim(e._id)],
|
|
93
|
+
['To', chalk.cyan(e.toEmail)],
|
|
94
|
+
['Subject', chalk.bold(e.subject)],
|
|
95
|
+
['Status', colorStatus(e.status)],
|
|
96
|
+
['From', e.fromEmail || '—'],
|
|
93
97
|
['Sent At', e.createdAt ? new Date(e.createdAt).toLocaleString() : '—'],
|
|
94
98
|
]
|
|
95
99
|
);
|
|
96
100
|
} catch (err) {
|
|
97
|
-
|
|
98
|
-
|
|
101
|
+
sp.fail(chalk.red('Failed'));
|
|
102
|
+
error(err.message);
|
|
99
103
|
process.exit(1);
|
|
100
104
|
}
|
|
101
105
|
});
|
package/lib/commands/keys.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
const { Command } = require('commander');
|
|
2
|
-
const ora = require('ora');
|
|
3
2
|
const chalk = require('chalk');
|
|
3
|
+
const _grad = require('gradient-string'); const gradient = _grad.default || _grad;
|
|
4
4
|
const client = require('../client');
|
|
5
|
-
const { table, json, info, success, error } = require('../output');
|
|
5
|
+
const { table, json, info, success, error, spinner } = require('../output');
|
|
6
6
|
|
|
7
7
|
const cmd = new Command('keys').description('Manage API keys');
|
|
8
8
|
|
|
@@ -11,26 +11,26 @@ cmd
|
|
|
11
11
|
.description('List all API keys')
|
|
12
12
|
.option('--json', 'Output raw JSON')
|
|
13
13
|
.action(async (opts) => {
|
|
14
|
-
const
|
|
14
|
+
const sp = spinner('Fetching API keys…').start();
|
|
15
15
|
try {
|
|
16
16
|
const data = await client.get('/user/keys');
|
|
17
|
-
|
|
17
|
+
sp.succeed(chalk.dim('Loaded'));
|
|
18
18
|
if (opts.json) return json(data);
|
|
19
19
|
const keys = data.keys || [];
|
|
20
|
-
if (!keys.length) return info('No API keys
|
|
20
|
+
if (!keys.length) return info('No API keys. Run: ' + chalk.cyan('sendcraft keys create <name>'));
|
|
21
21
|
table(
|
|
22
|
-
['ID', 'Name', 'Key
|
|
22
|
+
['ID', 'Name', 'Key', 'Permissions', 'Last Used', 'Created'],
|
|
23
23
|
keys.map(k => [
|
|
24
|
-
String(k._id).slice(-8),
|
|
25
|
-
k.name,
|
|
26
|
-
k.maskedKey || '***',
|
|
27
|
-
k.permissions
|
|
28
|
-
k.lastUsedAt ? new Date(k.lastUsedAt).toLocaleDateString() : 'Never',
|
|
29
|
-
k.createdAt
|
|
24
|
+
chalk.dim(String(k._id).slice(-8)),
|
|
25
|
+
chalk.bold(k.name),
|
|
26
|
+
chalk.dim(k.maskedKey || '***'),
|
|
27
|
+
k.permissions === 'full_access' ? chalk.green('full_access') : chalk.yellow('sending_access'),
|
|
28
|
+
k.lastUsedAt ? chalk.dim(new Date(k.lastUsedAt).toLocaleDateString()) : chalk.dim('Never'),
|
|
29
|
+
k.createdAt ? chalk.dim(new Date(k.createdAt).toLocaleDateString()) : '—',
|
|
30
30
|
])
|
|
31
31
|
);
|
|
32
32
|
} catch (err) {
|
|
33
|
-
|
|
33
|
+
sp.fail(chalk.red('Failed'));
|
|
34
34
|
error(err.message);
|
|
35
35
|
process.exit(1);
|
|
36
36
|
}
|
|
@@ -39,11 +39,11 @@ cmd
|
|
|
39
39
|
cmd
|
|
40
40
|
.command('create <name>')
|
|
41
41
|
.description('Create a new API key')
|
|
42
|
-
.option('--permissions <type>', 'full_access or sending_access', 'full_access')
|
|
43
|
-
.option('--domains <
|
|
42
|
+
.option('--permissions <type>', 'full_access (default) or sending_access', 'full_access')
|
|
43
|
+
.option('--domains <list>', 'Comma-separated allowed sender domains')
|
|
44
44
|
.option('--json', 'Output raw JSON')
|
|
45
45
|
.action(async (name, opts) => {
|
|
46
|
-
const
|
|
46
|
+
const sp = spinner(`Creating key "${chalk.cyan(name)}"…`).start();
|
|
47
47
|
try {
|
|
48
48
|
const body = {
|
|
49
49
|
name,
|
|
@@ -51,18 +51,22 @@ cmd
|
|
|
51
51
|
allowedDomains: opts.domains ? opts.domains.split(',').map(d => d.trim()) : undefined,
|
|
52
52
|
};
|
|
53
53
|
const data = await client.post('/user/keys', body);
|
|
54
|
-
|
|
54
|
+
sp.succeed(chalk.dim('Created'));
|
|
55
55
|
if (opts.json) return json(data);
|
|
56
|
+
|
|
56
57
|
const key = data.key;
|
|
57
|
-
|
|
58
|
-
console.log(chalk.bold('
|
|
59
|
-
console.log(chalk.
|
|
60
|
-
console.log(`\nPermissions: ${key.permissions}`);
|
|
58
|
+
console.log('\n' + gradient(['#6366f1', '#10b981'])(' ─── New API Key ───') + '\n');
|
|
59
|
+
console.log(` ${chalk.bold('Name:')} ${chalk.cyan(key.name)}`);
|
|
60
|
+
console.log(` ${chalk.bold('Permissions:')} ${key.permissions === 'full_access' ? chalk.green('full_access') : chalk.yellow('sending_access')}`);
|
|
61
61
|
if (key.allowedDomains?.length) {
|
|
62
|
-
console.log(`
|
|
62
|
+
console.log(` ${chalk.bold('Domains:')} ${key.allowedDomains.join(', ')}`);
|
|
63
63
|
}
|
|
64
|
+
console.log(`\n ${chalk.bold.yellow('Key (save this — shown once only):')}`);
|
|
65
|
+
console.log(` ${chalk.bgBlack.green.bold(' ' + (key.key || '—') + ' ')}\n`);
|
|
66
|
+
|
|
67
|
+
success('Store this key securely. It cannot be retrieved again.');
|
|
64
68
|
} catch (err) {
|
|
65
|
-
|
|
69
|
+
sp.fail(chalk.red('Failed'));
|
|
66
70
|
error(err.message);
|
|
67
71
|
process.exit(1);
|
|
68
72
|
}
|
|
@@ -70,17 +74,17 @@ cmd
|
|
|
70
74
|
|
|
71
75
|
cmd
|
|
72
76
|
.command('revoke <keyId>')
|
|
73
|
-
.description('
|
|
77
|
+
.description('Permanently revoke an API key')
|
|
74
78
|
.option('--json', 'Output raw JSON')
|
|
75
79
|
.action(async (keyId, opts) => {
|
|
76
|
-
const
|
|
80
|
+
const sp = spinner(`Revoking key ${chalk.dim(keyId.slice(-8))}…`).start();
|
|
77
81
|
try {
|
|
78
82
|
const data = await client.delete(`/user/keys/${keyId}`);
|
|
79
|
-
|
|
83
|
+
sp.succeed(chalk.dim('Revoked'));
|
|
80
84
|
if (opts.json) return json(data);
|
|
81
|
-
success(`Key ${keyId} revoked.`);
|
|
85
|
+
success(`Key ${chalk.dim(keyId)} permanently revoked.`);
|
|
82
86
|
} catch (err) {
|
|
83
|
-
|
|
87
|
+
sp.fail(chalk.red('Failed'));
|
|
84
88
|
error(err.message);
|
|
85
89
|
process.exit(1);
|
|
86
90
|
}
|