sendcraft-cli 1.0.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 +36 -0
- package/lib/client.js +63 -0
- package/lib/commands/campaigns.js +58 -0
- package/lib/commands/config.js +70 -0
- package/lib/commands/domains.js +121 -0
- package/lib/commands/emails.js +103 -0
- package/lib/commands/keys.js +89 -0
- package/lib/commands/send.js +40 -0
- package/lib/commands/subscribers.js +90 -0
- package/lib/commands/warmup.js +52 -0
- package/lib/config.js +33 -0
- package/lib/output.js +61 -0
- package/package.json +26 -0
package/bin/sendcraft.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
const { program } = require('commander');
|
|
6
|
+
const chalk = require('chalk');
|
|
7
|
+
const pkg = require('../package.json');
|
|
8
|
+
|
|
9
|
+
// Sub-commands
|
|
10
|
+
program
|
|
11
|
+
.name('sendcraft')
|
|
12
|
+
.description('Official SendCraft CLI')
|
|
13
|
+
.version(pkg.version, '-v, --version');
|
|
14
|
+
|
|
15
|
+
program.addCommand(require('../lib/commands/config'));
|
|
16
|
+
program.addCommand(require('../lib/commands/send'));
|
|
17
|
+
program.addCommand(require('../lib/commands/emails'));
|
|
18
|
+
program.addCommand(require('../lib/commands/campaigns'));
|
|
19
|
+
program.addCommand(require('../lib/commands/subscribers'));
|
|
20
|
+
program.addCommand(require('../lib/commands/domains'));
|
|
21
|
+
program.addCommand(require('../lib/commands/keys'));
|
|
22
|
+
program.addCommand(require('../lib/commands/warmup'));
|
|
23
|
+
|
|
24
|
+
// Friendly error on unknown command
|
|
25
|
+
program.on('command:*', (args) => {
|
|
26
|
+
console.error(chalk.red(`Unknown command: ${args[0]}`));
|
|
27
|
+
console.error('Run ' + chalk.bold('sendcraft --help') + ' to see available commands.');
|
|
28
|
+
process.exit(1);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
program.parse(process.argv);
|
|
32
|
+
|
|
33
|
+
// Show help if called with no args
|
|
34
|
+
if (process.argv.length < 3) {
|
|
35
|
+
program.help();
|
|
36
|
+
}
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin HTTP client wrapping Node's built-in https/http.
|
|
3
|
+
* No extra dependencies needed.
|
|
4
|
+
*/
|
|
5
|
+
const https = require('https');
|
|
6
|
+
const http = require('http');
|
|
7
|
+
const { URL } = require('url');
|
|
8
|
+
const { getApiKey, getBaseUrl } = require('./config');
|
|
9
|
+
|
|
10
|
+
function request(method, path, body = null) {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const apiKey = getApiKey();
|
|
13
|
+
if (!apiKey) {
|
|
14
|
+
return reject(new Error('No API key configured. Run: sendcraft config set-key <key>'));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const fullUrl = new URL(getBaseUrl() + path);
|
|
18
|
+
const isHttps = fullUrl.protocol === 'https:';
|
|
19
|
+
const mod = isHttps ? https : http;
|
|
20
|
+
|
|
21
|
+
const payload = body ? JSON.stringify(body) : null;
|
|
22
|
+
const options = {
|
|
23
|
+
hostname: fullUrl.hostname,
|
|
24
|
+
port: fullUrl.port || (isHttps ? 443 : 80),
|
|
25
|
+
path: fullUrl.pathname + fullUrl.search,
|
|
26
|
+
method,
|
|
27
|
+
headers: {
|
|
28
|
+
'x-api-key': apiKey,
|
|
29
|
+
'Content-Type': 'application/json',
|
|
30
|
+
...(payload ? { 'Content-Length': Buffer.byteLength(payload) } : {}),
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const req = mod.request(options, (res) => {
|
|
35
|
+
let data = '';
|
|
36
|
+
res.on('data', (chunk) => (data += chunk));
|
|
37
|
+
res.on('end', () => {
|
|
38
|
+
try {
|
|
39
|
+
const json = JSON.parse(data);
|
|
40
|
+
if (res.statusCode >= 400) {
|
|
41
|
+
const err = new Error(json.error || json.message || `HTTP ${res.statusCode}`);
|
|
42
|
+
err.status = res.statusCode;
|
|
43
|
+
err.body = json;
|
|
44
|
+
return reject(err);
|
|
45
|
+
}
|
|
46
|
+
resolve(json);
|
|
47
|
+
} catch {
|
|
48
|
+
reject(new Error(`Invalid JSON response (status ${res.statusCode})`));
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
req.on('error', reject);
|
|
54
|
+
if (payload) req.write(payload);
|
|
55
|
+
req.end();
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = {
|
|
60
|
+
get: (path) => request('GET', path),
|
|
61
|
+
post: (path, body) => request('POST', path, body),
|
|
62
|
+
delete: (path) => request('DELETE', path),
|
|
63
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const { Command } = require('commander');
|
|
2
|
+
const ora = require('ora');
|
|
3
|
+
const client = require('../client');
|
|
4
|
+
const { table, json, colorStatus, info, success, error } = require('../output');
|
|
5
|
+
|
|
6
|
+
const cmd = new Command('campaigns').description('Manage campaigns');
|
|
7
|
+
|
|
8
|
+
cmd
|
|
9
|
+
.command('list')
|
|
10
|
+
.description('List campaigns')
|
|
11
|
+
.option('--json', 'Output raw JSON')
|
|
12
|
+
.action(async (opts) => {
|
|
13
|
+
const spinner = ora('Fetching…').start();
|
|
14
|
+
try {
|
|
15
|
+
const data = await client.get('/campaigns');
|
|
16
|
+
spinner.stop();
|
|
17
|
+
if (opts.json) return json(data);
|
|
18
|
+
const campaigns = data.campaigns || [];
|
|
19
|
+
if (!campaigns.length) return info('No campaigns found.');
|
|
20
|
+
table(
|
|
21
|
+
['ID', 'Name', 'Subject', 'Status', 'Recipients', 'Created'],
|
|
22
|
+
campaigns.map(c => [
|
|
23
|
+
String(c._id).slice(-8),
|
|
24
|
+
(c.name || '').slice(0, 25),
|
|
25
|
+
(c.subject || '').slice(0, 30),
|
|
26
|
+
colorStatus(c.status),
|
|
27
|
+
c.recipientCount ?? '—',
|
|
28
|
+
c.createdAt ? new Date(c.createdAt).toLocaleString() : '—',
|
|
29
|
+
])
|
|
30
|
+
);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
spinner.stop();
|
|
33
|
+
error(err.message);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
cmd
|
|
39
|
+
.command('send <campaignId>')
|
|
40
|
+
.description('Send or schedule a campaign')
|
|
41
|
+
.option('--schedule <isoDate>', 'Schedule for a specific time (ISO 8601)')
|
|
42
|
+
.option('--json', 'Output raw JSON')
|
|
43
|
+
.action(async (campaignId, opts) => {
|
|
44
|
+
const spinner = ora('Sending…').start();
|
|
45
|
+
try {
|
|
46
|
+
const body = opts.schedule ? { scheduledAt: opts.schedule } : {};
|
|
47
|
+
const data = await client.post(`/campaigns/${campaignId}/send`, body);
|
|
48
|
+
spinner.stop();
|
|
49
|
+
if (opts.json) return json(data);
|
|
50
|
+
success(`Campaign ${campaignId} ${opts.schedule ? 'scheduled' : 'sent'}!`);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
spinner.stop();
|
|
53
|
+
error(err.message);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
module.exports = cmd;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const { Command } = require('commander');
|
|
2
|
+
const prompts = require('prompts');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const { load, save, getApiKey, getBaseUrl, CONFIG_FILE } = require('../config');
|
|
5
|
+
const { success, info, error } = require('../output');
|
|
6
|
+
|
|
7
|
+
const cmd = new Command('config')
|
|
8
|
+
.description('Configure your SendCraft credentials');
|
|
9
|
+
|
|
10
|
+
cmd
|
|
11
|
+
.command('set-key <apiKey>')
|
|
12
|
+
.description('Save your API key')
|
|
13
|
+
.action((apiKey) => {
|
|
14
|
+
const cfg = load();
|
|
15
|
+
cfg.api_key = apiKey;
|
|
16
|
+
save(cfg);
|
|
17
|
+
success(`API key saved to ${CONFIG_FILE}`);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
cmd
|
|
21
|
+
.command('set-url <url>')
|
|
22
|
+
.description('Override the API base URL (default: https://api.sendcraft.online/api)')
|
|
23
|
+
.action((url) => {
|
|
24
|
+
const cfg = load();
|
|
25
|
+
cfg.base_url = url;
|
|
26
|
+
save(cfg);
|
|
27
|
+
success(`Base URL saved: ${url}`);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
cmd
|
|
31
|
+
.command('show')
|
|
32
|
+
.description('Show current configuration')
|
|
33
|
+
.action(() => {
|
|
34
|
+
const key = getApiKey();
|
|
35
|
+
const url = getBaseUrl();
|
|
36
|
+
if (!key) {
|
|
37
|
+
error('No API key set. Run: sendcraft config set-key <key>');
|
|
38
|
+
info('Or set SENDCRAFT_API_KEY env variable.');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
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);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
cmd
|
|
48
|
+
.command('init')
|
|
49
|
+
.description('Interactive setup')
|
|
50
|
+
.action(async () => {
|
|
51
|
+
const answers = await prompts([
|
|
52
|
+
{
|
|
53
|
+
type: 'password',
|
|
54
|
+
name: 'api_key',
|
|
55
|
+
message: 'Paste your SendCraft API key:',
|
|
56
|
+
validate: v => v.startsWith('sc_') ? true : 'Key should start with sc_',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
type: 'text',
|
|
60
|
+
name: 'base_url',
|
|
61
|
+
message: 'API base URL',
|
|
62
|
+
initial: 'https://api.sendcraft.online/api',
|
|
63
|
+
},
|
|
64
|
+
]);
|
|
65
|
+
if (!answers.api_key) return error('Cancelled.');
|
|
66
|
+
save(answers);
|
|
67
|
+
success('Config saved!');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
module.exports = cmd;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
const { Command } = require('commander');
|
|
2
|
+
const ora = require('ora');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const client = require('../client');
|
|
5
|
+
const { table, json, colorStatus, info, success, error } = require('../output');
|
|
6
|
+
|
|
7
|
+
const cmd = new Command('domains').description('Manage sender domains');
|
|
8
|
+
|
|
9
|
+
cmd
|
|
10
|
+
.command('list')
|
|
11
|
+
.description('List all domains')
|
|
12
|
+
.option('--json', 'Output raw JSON')
|
|
13
|
+
.action(async (opts) => {
|
|
14
|
+
const spinner = ora('Fetching…').start();
|
|
15
|
+
try {
|
|
16
|
+
const data = await client.get('/domains');
|
|
17
|
+
spinner.stop();
|
|
18
|
+
if (opts.json) return json(data);
|
|
19
|
+
const domains = data.domains || [];
|
|
20
|
+
if (!domains.length) return info('No domains added yet.');
|
|
21
|
+
table(
|
|
22
|
+
['Domain', 'Status', 'SPF', 'DKIM', 'DMARC', 'Added'],
|
|
23
|
+
domains.map(d => [
|
|
24
|
+
d.domain,
|
|
25
|
+
colorStatus(d.status),
|
|
26
|
+
d.spfVerified ? chalk.green('✓') : chalk.red('✗'),
|
|
27
|
+
d.dkimVerified ? chalk.green('✓') : chalk.red('✗'),
|
|
28
|
+
d.dmarcVerified ? chalk.green('✓') : chalk.red('✗'),
|
|
29
|
+
d.createdAt ? new Date(d.createdAt).toLocaleDateString() : '—',
|
|
30
|
+
])
|
|
31
|
+
);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
spinner.stop();
|
|
34
|
+
error(err.message);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
cmd
|
|
40
|
+
.command('add <domain>')
|
|
41
|
+
.description('Add a new sender domain')
|
|
42
|
+
.option('--json', 'Output raw JSON')
|
|
43
|
+
.action(async (domain, opts) => {
|
|
44
|
+
const spinner = ora('Adding domain…').start();
|
|
45
|
+
try {
|
|
46
|
+
const data = await client.post('/domains', { domain });
|
|
47
|
+
spinner.stop();
|
|
48
|
+
if (opts.json) return json(data);
|
|
49
|
+
success(`Domain ${domain} added.`);
|
|
50
|
+
info('Add these DNS records to your domain:');
|
|
51
|
+
(data.dnsRecords || []).forEach(r => {
|
|
52
|
+
const optional = r.optional ? chalk.gray(' (optional)') : '';
|
|
53
|
+
console.log(`\n ${chalk.bold(r.purpose)}${optional}`);
|
|
54
|
+
console.log(` Type: ${r.type}`);
|
|
55
|
+
console.log(` Name: ${r.name}`);
|
|
56
|
+
console.log(` Value: ${r.value}`);
|
|
57
|
+
});
|
|
58
|
+
} catch (err) {
|
|
59
|
+
spinner.stop();
|
|
60
|
+
error(err.message);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
cmd
|
|
66
|
+
.command('verify <domainId>')
|
|
67
|
+
.description('Trigger a DNS verification check')
|
|
68
|
+
.option('--json', 'Output raw JSON')
|
|
69
|
+
.action(async (id, opts) => {
|
|
70
|
+
const spinner = ora('Checking DNS…').start();
|
|
71
|
+
try {
|
|
72
|
+
const data = await client.post(`/domains/${id}/verify`);
|
|
73
|
+
spinner.stop();
|
|
74
|
+
if (opts.json) return json(data);
|
|
75
|
+
if (data.verified) {
|
|
76
|
+
success('All records verified! Domain is ready.');
|
|
77
|
+
} else {
|
|
78
|
+
const r = data.results || {};
|
|
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.');
|
|
88
|
+
}
|
|
89
|
+
} catch (err) {
|
|
90
|
+
spinner.stop();
|
|
91
|
+
error(err.message);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
cmd
|
|
97
|
+
.command('records <domainId>')
|
|
98
|
+
.description('Show DNS records to configure for a domain')
|
|
99
|
+
.option('--json', 'Output raw JSON')
|
|
100
|
+
.action(async (id, opts) => {
|
|
101
|
+
const spinner = ora('Loading…').start();
|
|
102
|
+
try {
|
|
103
|
+
const data = await client.get(`/domains/${id}`);
|
|
104
|
+
spinner.stop();
|
|
105
|
+
if (opts.json) return json(data.dnsRecords);
|
|
106
|
+
(data.dnsRecords || []).forEach(r => {
|
|
107
|
+
const status = r.verified ? chalk.green('✓ verified') : chalk.yellow('⏳ pending');
|
|
108
|
+
const optional = r.optional ? chalk.gray(' (optional)') : '';
|
|
109
|
+
console.log(`\n ${chalk.bold(r.purpose)} — ${status}${optional}`);
|
|
110
|
+
console.log(` Type: ${r.type}`);
|
|
111
|
+
console.log(` Name: ${r.name}`);
|
|
112
|
+
console.log(` Value: ${r.value}`);
|
|
113
|
+
});
|
|
114
|
+
} catch (err) {
|
|
115
|
+
spinner.stop();
|
|
116
|
+
error(err.message);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
module.exports = cmd;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
const { Command } = require('commander');
|
|
2
|
+
const ora = require('ora');
|
|
3
|
+
const client = require('../client');
|
|
4
|
+
const { table, json, colorStatus, info } = require('../output');
|
|
5
|
+
|
|
6
|
+
const cmd = new Command('emails').description('Manage emails');
|
|
7
|
+
|
|
8
|
+
cmd
|
|
9
|
+
.command('list')
|
|
10
|
+
.description('List sent emails')
|
|
11
|
+
.option('-p, --page <n>', 'Page number', '1')
|
|
12
|
+
.option('-l, --limit <n>', 'Items per page', '20')
|
|
13
|
+
.option('--status <s>', 'Filter by status (sent, delivered, failed, scheduled)')
|
|
14
|
+
.option('--json', 'Output raw JSON')
|
|
15
|
+
.action(async (opts) => {
|
|
16
|
+
const spinner = ora('Fetching…').start();
|
|
17
|
+
try {
|
|
18
|
+
const params = new URLSearchParams({ page: opts.page, limit: opts.limit });
|
|
19
|
+
if (opts.status) params.set('status', opts.status);
|
|
20
|
+
const data = await client.get(`/emails?${params}`);
|
|
21
|
+
spinner.stop();
|
|
22
|
+
if (opts.json) return json(data);
|
|
23
|
+
const emails = data.emails || [];
|
|
24
|
+
if (!emails.length) return info('No emails found.');
|
|
25
|
+
table(
|
|
26
|
+
['ID', 'To', 'Subject', 'Status', 'Sent At'],
|
|
27
|
+
emails.map(e => [
|
|
28
|
+
String(e._id).slice(-8),
|
|
29
|
+
e.toEmail,
|
|
30
|
+
(e.subject || '').slice(0, 40),
|
|
31
|
+
colorStatus(e.status),
|
|
32
|
+
e.createdAt ? new Date(e.createdAt).toLocaleString() : '—',
|
|
33
|
+
])
|
|
34
|
+
);
|
|
35
|
+
info(`Page ${opts.page} · ${emails.length} of ${data.total || '?'}`);
|
|
36
|
+
} catch (err) {
|
|
37
|
+
spinner.stop();
|
|
38
|
+
require('../output').error(err.message);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
cmd
|
|
44
|
+
.command('stats')
|
|
45
|
+
.description('Show email stats summary')
|
|
46
|
+
.option('--json', 'Output raw JSON')
|
|
47
|
+
.action(async (opts) => {
|
|
48
|
+
const spinner = ora('Loading…').start();
|
|
49
|
+
try {
|
|
50
|
+
const data = await client.get('/emails/stats/summary');
|
|
51
|
+
spinner.stop();
|
|
52
|
+
if (opts.json) return json(data);
|
|
53
|
+
const s = data.stats || data;
|
|
54
|
+
table(
|
|
55
|
+
['Metric', 'Value'],
|
|
56
|
+
[
|
|
57
|
+
['Total Sent', s.totalSent ?? '—'],
|
|
58
|
+
['Delivered', s.delivered ?? '—'],
|
|
59
|
+
['Opened', s.opened ?? '—'],
|
|
60
|
+
['Clicked', s.clicked ?? '—'],
|
|
61
|
+
['Bounced', s.bounced ?? '—'],
|
|
62
|
+
['Failed', s.failed ?? '—'],
|
|
63
|
+
['Open Rate', s.openRate != null ? `${(s.openRate * 100).toFixed(1)}%` : '—'],
|
|
64
|
+
['Click Rate', s.clickRate != null ? `${(s.clickRate * 100).toFixed(1)}%` : '—'],
|
|
65
|
+
]
|
|
66
|
+
);
|
|
67
|
+
} catch (err) {
|
|
68
|
+
spinner.stop();
|
|
69
|
+
require('../output').error(err.message);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
cmd
|
|
75
|
+
.command('get <emailId>')
|
|
76
|
+
.description('Get details for a single email')
|
|
77
|
+
.option('--json', 'Output raw JSON')
|
|
78
|
+
.action(async (emailId, opts) => {
|
|
79
|
+
const spinner = ora('Fetching…').start();
|
|
80
|
+
try {
|
|
81
|
+
const data = await client.get(`/emails/${emailId}`);
|
|
82
|
+
spinner.stop();
|
|
83
|
+
if (opts.json) return json(data);
|
|
84
|
+
const e = data.email || data;
|
|
85
|
+
table(
|
|
86
|
+
['Field', 'Value'],
|
|
87
|
+
[
|
|
88
|
+
['ID', e._id],
|
|
89
|
+
['To', e.toEmail],
|
|
90
|
+
['Subject', e.subject],
|
|
91
|
+
['Status', colorStatus(e.status)],
|
|
92
|
+
['From', e.fromEmail || '—'],
|
|
93
|
+
['Sent At', e.createdAt ? new Date(e.createdAt).toLocaleString() : '—'],
|
|
94
|
+
]
|
|
95
|
+
);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
spinner.stop();
|
|
98
|
+
require('../output').error(err.message);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
module.exports = cmd;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const { Command } = require('commander');
|
|
2
|
+
const ora = require('ora');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const client = require('../client');
|
|
5
|
+
const { table, json, info, success, error } = require('../output');
|
|
6
|
+
|
|
7
|
+
const cmd = new Command('keys').description('Manage API keys');
|
|
8
|
+
|
|
9
|
+
cmd
|
|
10
|
+
.command('list')
|
|
11
|
+
.description('List all API keys')
|
|
12
|
+
.option('--json', 'Output raw JSON')
|
|
13
|
+
.action(async (opts) => {
|
|
14
|
+
const spinner = ora('Fetching…').start();
|
|
15
|
+
try {
|
|
16
|
+
const data = await client.get('/user/keys');
|
|
17
|
+
spinner.stop();
|
|
18
|
+
if (opts.json) return json(data);
|
|
19
|
+
const keys = data.keys || [];
|
|
20
|
+
if (!keys.length) return info('No API keys found.');
|
|
21
|
+
table(
|
|
22
|
+
['ID', 'Name', 'Key (masked)', 'Permissions', 'Last Used', 'Created'],
|
|
23
|
+
keys.map(k => [
|
|
24
|
+
String(k._id).slice(-8),
|
|
25
|
+
k.name,
|
|
26
|
+
k.maskedKey || '***',
|
|
27
|
+
k.permissions || 'full_access',
|
|
28
|
+
k.lastUsedAt ? new Date(k.lastUsedAt).toLocaleDateString() : 'Never',
|
|
29
|
+
k.createdAt ? new Date(k.createdAt).toLocaleDateString() : '—',
|
|
30
|
+
])
|
|
31
|
+
);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
spinner.stop();
|
|
34
|
+
error(err.message);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
cmd
|
|
40
|
+
.command('create <name>')
|
|
41
|
+
.description('Create a new API key')
|
|
42
|
+
.option('--permissions <type>', 'full_access or sending_access', 'full_access')
|
|
43
|
+
.option('--domains <domains>', 'Comma-separated allowed sender domains')
|
|
44
|
+
.option('--json', 'Output raw JSON')
|
|
45
|
+
.action(async (name, opts) => {
|
|
46
|
+
const spinner = ora('Creating…').start();
|
|
47
|
+
try {
|
|
48
|
+
const body = {
|
|
49
|
+
name,
|
|
50
|
+
permissions: opts.permissions,
|
|
51
|
+
allowedDomains: opts.domains ? opts.domains.split(',').map(d => d.trim()) : undefined,
|
|
52
|
+
};
|
|
53
|
+
const data = await client.post('/user/keys', body);
|
|
54
|
+
spinner.stop();
|
|
55
|
+
if (opts.json) return json(data);
|
|
56
|
+
const key = data.key;
|
|
57
|
+
success('API key created!');
|
|
58
|
+
console.log(chalk.bold('\nKey (save this — shown only once):'));
|
|
59
|
+
console.log(chalk.green(key.key || '—'));
|
|
60
|
+
console.log(`\nPermissions: ${key.permissions}`);
|
|
61
|
+
if (key.allowedDomains?.length) {
|
|
62
|
+
console.log(`Allowed domains: ${key.allowedDomains.join(', ')}`);
|
|
63
|
+
}
|
|
64
|
+
} catch (err) {
|
|
65
|
+
spinner.stop();
|
|
66
|
+
error(err.message);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
cmd
|
|
72
|
+
.command('revoke <keyId>')
|
|
73
|
+
.description('Revoke an API key permanently')
|
|
74
|
+
.option('--json', 'Output raw JSON')
|
|
75
|
+
.action(async (keyId, opts) => {
|
|
76
|
+
const spinner = ora('Revoking…').start();
|
|
77
|
+
try {
|
|
78
|
+
const data = await client.delete(`/user/keys/${keyId}`);
|
|
79
|
+
spinner.stop();
|
|
80
|
+
if (opts.json) return json(data);
|
|
81
|
+
success(`Key ${keyId} revoked.`);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
spinner.stop();
|
|
84
|
+
error(err.message);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
module.exports = cmd;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const { Command } = require('commander');
|
|
2
|
+
const ora = require('ora');
|
|
3
|
+
const client = require('../client');
|
|
4
|
+
const { success, error, json } = require('../output');
|
|
5
|
+
|
|
6
|
+
const cmd = new Command('send')
|
|
7
|
+
.description('Send a single transactional email')
|
|
8
|
+
.requiredOption('-t, --to <email>', 'Recipient email address')
|
|
9
|
+
.requiredOption('-s, --subject <text>', 'Email subject')
|
|
10
|
+
.option('-H, --html <html>', 'HTML body')
|
|
11
|
+
.option('-T, --text <text>', 'Plain text body')
|
|
12
|
+
.option('-f, --from <email>', 'From address (uses account default if omitted)')
|
|
13
|
+
.option('-r, --reply-to <email>', 'Reply-To address')
|
|
14
|
+
.option('--json', 'Output raw JSON')
|
|
15
|
+
.action(async (opts) => {
|
|
16
|
+
if (!opts.html && !opts.text) {
|
|
17
|
+
error('Provide at least --html or --text');
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
const spinner = ora('Sending…').start();
|
|
21
|
+
try {
|
|
22
|
+
const result = await client.post('/emails/send', {
|
|
23
|
+
toEmail: opts.to,
|
|
24
|
+
subject: opts.subject,
|
|
25
|
+
htmlContent: opts.html,
|
|
26
|
+
plainTextContent: opts.text,
|
|
27
|
+
fromEmail: opts.from,
|
|
28
|
+
replyTo: opts.replyTo,
|
|
29
|
+
});
|
|
30
|
+
spinner.stop();
|
|
31
|
+
if (opts.json) return json(result);
|
|
32
|
+
success(`Email sent! ID: ${result.emailId || result._id || '—'}`);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
spinner.stop();
|
|
35
|
+
error(err.message);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
module.exports = cmd;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const { Command } = require('commander');
|
|
2
|
+
const ora = require('ora');
|
|
3
|
+
const client = require('../client');
|
|
4
|
+
const { table, json, colorStatus, info, success, error } = require('../output');
|
|
5
|
+
|
|
6
|
+
const cmd = new Command('subscribers').description('Manage subscribers');
|
|
7
|
+
|
|
8
|
+
cmd
|
|
9
|
+
.command('list')
|
|
10
|
+
.description('List subscribers')
|
|
11
|
+
.option('-p, --page <n>', 'Page', '1')
|
|
12
|
+
.option('-l, --limit <n>', 'Limit', '20')
|
|
13
|
+
.option('--status <s>', 'Filter: active, pending, unsubscribed')
|
|
14
|
+
.option('--json', 'Output raw JSON')
|
|
15
|
+
.action(async (opts) => {
|
|
16
|
+
const spinner = ora('Fetching…').start();
|
|
17
|
+
try {
|
|
18
|
+
const params = new URLSearchParams({ page: opts.page, limit: opts.limit });
|
|
19
|
+
if (opts.status) params.set('status', opts.status);
|
|
20
|
+
const data = await client.get(`/subscribers?${params}`);
|
|
21
|
+
spinner.stop();
|
|
22
|
+
if (opts.json) return json(data);
|
|
23
|
+
const subs = data.subscribers || [];
|
|
24
|
+
if (!subs.length) return info('No subscribers found.');
|
|
25
|
+
table(
|
|
26
|
+
['Email', 'Name', 'Status', 'Tags', 'Joined'],
|
|
27
|
+
subs.map(s => [
|
|
28
|
+
s.email,
|
|
29
|
+
[s.firstName, s.lastName].filter(Boolean).join(' ') || '—',
|
|
30
|
+
colorStatus(s.status),
|
|
31
|
+
(s.tags || []).join(', ') || '—',
|
|
32
|
+
s.createdAt ? new Date(s.createdAt).toLocaleDateString() : '—',
|
|
33
|
+
])
|
|
34
|
+
);
|
|
35
|
+
info(`Page ${opts.page} · ${subs.length} of ${data.total || '?'}`);
|
|
36
|
+
} catch (err) {
|
|
37
|
+
spinner.stop();
|
|
38
|
+
error(err.message);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
cmd
|
|
44
|
+
.command('add <email>')
|
|
45
|
+
.description('Add a subscriber')
|
|
46
|
+
.option('--list <listId>', 'Email list ID')
|
|
47
|
+
.option('--first-name <name>', 'First name')
|
|
48
|
+
.option('--last-name <name>', 'Last name')
|
|
49
|
+
.option('--tags <tags>', 'Comma-separated tags')
|
|
50
|
+
.option('--json', 'Output raw JSON')
|
|
51
|
+
.action(async (email, opts) => {
|
|
52
|
+
if (!opts.list) { error('--list <listId> is required'); process.exit(1); }
|
|
53
|
+
const spinner = ora('Adding…').start();
|
|
54
|
+
try {
|
|
55
|
+
const data = await client.post('/subscribers/add', {
|
|
56
|
+
email,
|
|
57
|
+
listId: opts.list,
|
|
58
|
+
firstName: opts.firstName,
|
|
59
|
+
lastName: opts.lastName,
|
|
60
|
+
tags: opts.tags ? opts.tags.split(',').map(t => t.trim()) : undefined,
|
|
61
|
+
});
|
|
62
|
+
spinner.stop();
|
|
63
|
+
if (opts.json) return json(data);
|
|
64
|
+
success(`Subscriber ${email} added.`);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
spinner.stop();
|
|
67
|
+
error(err.message);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
cmd
|
|
73
|
+
.command('remove <subscriberId>')
|
|
74
|
+
.description('Remove a subscriber by ID')
|
|
75
|
+
.option('--json', 'Output raw JSON')
|
|
76
|
+
.action(async (id, opts) => {
|
|
77
|
+
const spinner = ora('Removing…').start();
|
|
78
|
+
try {
|
|
79
|
+
const data = await client.delete(`/subscribers/${id}`);
|
|
80
|
+
spinner.stop();
|
|
81
|
+
if (opts.json) return json(data);
|
|
82
|
+
success(`Subscriber ${id} removed.`);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
spinner.stop();
|
|
85
|
+
error(err.message);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
module.exports = cmd;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const { Command } = require('commander');
|
|
2
|
+
const ora = require('ora');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const client = require('../client');
|
|
5
|
+
const { table, json, error } = require('../output');
|
|
6
|
+
|
|
7
|
+
const cmd = new Command('warmup').description('View SMTP IP warmup status');
|
|
8
|
+
|
|
9
|
+
cmd
|
|
10
|
+
.option('--json', 'Output raw JSON')
|
|
11
|
+
.action(async (opts) => {
|
|
12
|
+
const spinner = ora('Loading warmup status…').start();
|
|
13
|
+
try {
|
|
14
|
+
const data = await client.get('/smtp/warmup');
|
|
15
|
+
spinner.stop();
|
|
16
|
+
if (opts.json) return json(data);
|
|
17
|
+
|
|
18
|
+
const bar = buildBar(data.todayCount, data.dailyLimit);
|
|
19
|
+
|
|
20
|
+
if (data.isWarmedUp) {
|
|
21
|
+
console.log(chalk.green.bold('✓ IP is fully warmed up — no daily limits'));
|
|
22
|
+
} else {
|
|
23
|
+
console.log(chalk.yellow.bold(`Warmup Day ${data.warmupDay}`));
|
|
24
|
+
console.log(`Progress: ${bar} ${data.todayCount}/${data.dailyLimit}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
table(
|
|
28
|
+
['Field', 'Value'],
|
|
29
|
+
[
|
|
30
|
+
['Warmup Day', data.warmupDay],
|
|
31
|
+
['Daily Limit', data.isWarmedUp ? 'Unlimited' : data.dailyLimit],
|
|
32
|
+
['Sent Today', data.todayCount],
|
|
33
|
+
['Remaining Today', data.isWarmedUp ? '∞' : data.remainingToday],
|
|
34
|
+
['Warmed Up', data.isWarmedUp ? chalk.green('Yes') : chalk.yellow('No')],
|
|
35
|
+
['% Complete', data.percentComplete != null ? `${data.percentComplete}%` : '—'],
|
|
36
|
+
]
|
|
37
|
+
);
|
|
38
|
+
} catch (err) {
|
|
39
|
+
spinner.stop();
|
|
40
|
+
error(err.message);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
function buildBar(count, limit) {
|
|
46
|
+
if (!limit) return '';
|
|
47
|
+
const width = 20;
|
|
48
|
+
const filled = Math.round((count / limit) * width);
|
|
49
|
+
return '[' + chalk.green('█'.repeat(filled)) + '░'.repeat(width - filled) + ']';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = cmd;
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads/writes ~/.sendcraft/config.json
|
|
3
|
+
* Stores: api_key, base_url
|
|
4
|
+
*/
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
|
|
9
|
+
const CONFIG_DIR = path.join(os.homedir(), '.sendcraft');
|
|
10
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
11
|
+
|
|
12
|
+
function load() {
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
15
|
+
} catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function save(data) {
|
|
21
|
+
if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
22
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2), 'utf8');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getApiKey() {
|
|
26
|
+
return process.env.SENDCRAFT_API_KEY || load().api_key || null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getBaseUrl() {
|
|
30
|
+
return process.env.SENDCRAFT_BASE_URL || load().base_url || 'https://api.sendcraft.online/api';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = { load, save, getApiKey, getBaseUrl, CONFIG_FILE };
|
package/lib/output.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formatting helpers — tables, status badges, JSON flag support.
|
|
3
|
+
*/
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const Table = require('cli-table3');
|
|
6
|
+
|
|
7
|
+
const STATUS_COLORS = {
|
|
8
|
+
delivered: 'green',
|
|
9
|
+
sent: 'green',
|
|
10
|
+
active: 'green',
|
|
11
|
+
verified: 'green',
|
|
12
|
+
warmed_up: 'green',
|
|
13
|
+
opened: 'cyan',
|
|
14
|
+
clicked: 'cyan',
|
|
15
|
+
pending: 'yellow',
|
|
16
|
+
scheduled: 'yellow',
|
|
17
|
+
draft: 'yellow',
|
|
18
|
+
failed: 'red',
|
|
19
|
+
bounced: 'red',
|
|
20
|
+
complained: 'red',
|
|
21
|
+
cancelled: 'gray',
|
|
22
|
+
paused: 'gray',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function colorStatus(status) {
|
|
26
|
+
const color = STATUS_COLORS[status] || 'white';
|
|
27
|
+
return chalk[color](status);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function table(headers, rows) {
|
|
31
|
+
const t = new Table({ head: headers.map(h => chalk.bold(h)), style: { compact: true } });
|
|
32
|
+
rows.forEach(row => t.push(row));
|
|
33
|
+
console.log(t.toString());
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function json(data) {
|
|
37
|
+
console.log(JSON.stringify(data, null, 2));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function success(msg) {
|
|
41
|
+
console.log(chalk.green('✓') + ' ' + msg);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function error(msg) {
|
|
45
|
+
console.error(chalk.red('✗') + ' ' + msg);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function info(msg) {
|
|
49
|
+
console.log(chalk.blue('ℹ') + ' ' + msg);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function warn(msg) {
|
|
53
|
+
console.log(chalk.yellow('⚠') + ' ' + msg);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function printOrJson(data, asJson, printFn) {
|
|
57
|
+
if (asJson) return json(data);
|
|
58
|
+
printFn(data);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = { table, json, success, error, info, warn, printOrJson, colorStatus };
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sendcraft-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Official SendCraft CLI — send emails, manage campaigns, and more from your terminal",
|
|
5
|
+
"bin": {
|
|
6
|
+
"sendcraft": "./bin/sendcraft.js"
|
|
7
|
+
},
|
|
8
|
+
"main": "./bin/sendcraft.js",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "echo \"No tests yet\""
|
|
11
|
+
},
|
|
12
|
+
"keywords": ["sendcraft", "email", "cli", "transactional"],
|
|
13
|
+
"author": "SendCraft Team <sendcraft.team@gmail.com>",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"chalk": "^4.1.2",
|
|
17
|
+
"cli-table3": "^0.6.5",
|
|
18
|
+
"commander": "^11.1.0",
|
|
19
|
+
"ora": "^5.4.1",
|
|
20
|
+
"prompts": "^2.4.2"
|
|
21
|
+
},
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=14.0.0"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://sendcraft.online"
|
|
26
|
+
}
|