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.
@@ -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
+ }