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/lib/client.js CHANGED
@@ -7,11 +7,94 @@ const http = require('http');
7
7
  const { URL } = require('url');
8
8
  const { getApiKey, getBaseUrl } = require('./config');
9
9
 
10
+ /** Make a request with an explicit bearer token instead of stored API key */
11
+ function requestWithToken(method, path, body, token) {
12
+ return new Promise((resolve, reject) => {
13
+ const fullUrl = new URL(getBaseUrl() + path);
14
+ const isHttps = fullUrl.protocol === 'https:';
15
+ const mod = isHttps ? require('https') : require('http');
16
+ const payload = body ? JSON.stringify(body) : null;
17
+ const options = {
18
+ hostname: fullUrl.hostname,
19
+ port: fullUrl.port || (isHttps ? 443 : 80),
20
+ path: fullUrl.pathname + fullUrl.search,
21
+ method,
22
+ headers: {
23
+ 'Authorization': `Bearer ${token}`,
24
+ 'Content-Type': 'application/json',
25
+ ...(payload ? { 'Content-Length': Buffer.byteLength(payload) } : {}),
26
+ },
27
+ };
28
+ const req = mod.request(options, (res) => {
29
+ let data = '';
30
+ res.on('data', (chunk) => (data += chunk));
31
+ res.on('end', () => {
32
+ try {
33
+ const json = JSON.parse(data);
34
+ if (res.statusCode >= 400) {
35
+ const err = new Error(json.error || json.message || `HTTP ${res.statusCode}`);
36
+ err.status = res.statusCode;
37
+ err.body = json;
38
+ return reject(err);
39
+ }
40
+ resolve(json);
41
+ } catch {
42
+ reject(new Error(`Invalid JSON response (status ${res.statusCode})`));
43
+ }
44
+ });
45
+ });
46
+ req.on('error', reject);
47
+ if (payload) req.write(payload);
48
+ req.end();
49
+ });
50
+ }
51
+
52
+ /** Make a request with no auth header (for public endpoints like /auth/login) */
53
+ function requestNoAuth(method, path, body = null) {
54
+ return new Promise((resolve, reject) => {
55
+ const fullUrl = new URL(getBaseUrl() + path);
56
+ const isHttps = fullUrl.protocol === 'https:';
57
+ const mod = isHttps ? require('https') : require('http');
58
+ const payload = body ? JSON.stringify(body) : null;
59
+ const options = {
60
+ hostname: fullUrl.hostname,
61
+ port: fullUrl.port || (isHttps ? 443 : 80),
62
+ path: fullUrl.pathname + fullUrl.search,
63
+ method,
64
+ headers: {
65
+ 'Content-Type': 'application/json',
66
+ ...(payload ? { 'Content-Length': Buffer.byteLength(payload) } : {}),
67
+ },
68
+ };
69
+ const req = mod.request(options, (res) => {
70
+ let data = '';
71
+ res.on('data', (chunk) => (data += chunk));
72
+ res.on('end', () => {
73
+ try {
74
+ const json = JSON.parse(data);
75
+ if (res.statusCode >= 400) {
76
+ const err = new Error(json.error || json.message || `HTTP ${res.statusCode}`);
77
+ err.status = res.statusCode;
78
+ err.body = json;
79
+ return reject(err);
80
+ }
81
+ resolve(json);
82
+ } catch {
83
+ reject(new Error(`Invalid JSON response (status ${res.statusCode})`));
84
+ }
85
+ });
86
+ });
87
+ req.on('error', reject);
88
+ if (payload) req.write(payload);
89
+ req.end();
90
+ });
91
+ }
92
+
10
93
  function request(method, path, body = null) {
11
94
  return new Promise((resolve, reject) => {
12
95
  const apiKey = getApiKey();
13
96
  if (!apiKey) {
14
- return reject(new Error('No API key configured. Run: sendcraft config set-key <key>'));
97
+ return reject(new Error('No API key configured. Run: sendcraft login'));
15
98
  }
16
99
 
17
100
  const fullUrl = new URL(getBaseUrl() + path);
@@ -60,4 +143,7 @@ module.exports = {
60
143
  get: (path) => request('GET', path),
61
144
  post: (path, body) => request('POST', path, body),
62
145
  delete: (path) => request('DELETE', path),
146
+ postNoAuth: (path, body) => requestNoAuth('POST', path, body),
147
+ postWithToken: (path, body, token) => requestWithToken('POST', path, body, token),
148
+ getWithToken: (path, token) => requestWithToken('GET', path, null, token),
63
149
  };
@@ -1,7 +1,7 @@
1
1
  const { Command } = require('commander');
2
- const ora = require('ora');
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 spinner = ora('Fetching…').start();
13
+ const sp = spinner('Fetching campaigns…').start();
14
14
  try {
15
15
  const data = await client.get('/campaigns');
16
- spinner.stop();
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 found.');
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
- spinner.stop();
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 time (ISO 8601)')
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 spinner = ora('Sending…').start();
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
- spinner.stop();
49
+ sp.succeed(chalk.dim('Done'));
49
50
  if (opts.json) return json(data);
50
- success(`Campaign ${campaignId} ${opts.schedule ? 'scheduled' : 'sent'}!`);
51
+ success(`Campaign ${opts.schedule ? 'scheduled for ' + chalk.cyan(opts.schedule) : 'sent!'}`);
51
52
  } catch (err) {
52
- spinner.stop();
53
+ sp.fail(chalk.red('Failed'));
53
54
  error(err.message);
54
55
  process.exit(1);
55
56
  }
@@ -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, info, error } = require('../output');
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
- success(`API key saved to ${CONFIG_FILE}`);
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 (default: https://api.sendcraft.online/api)')
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 saved: ${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 env variable.');
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 SendCraft API key:',
56
- validate: v => v.startsWith('sc_') ? true : 'Key should start with sc_',
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
- if (!answers.api_key) return error('Cancelled.');
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
- success('Config saved!');
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;
@@ -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 spinner = ora('Fetching…').start();
14
+ const sp = spinner('Fetching domains…').start();
15
15
  try {
16
16
  const data = await client.get('/domains');
17
- spinner.stop();
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 ? chalk.green('✓') : chalk.red('✗'),
26
+ d.spfVerified ? chalk.green('✓') : chalk.red('✗'),
27
27
  d.dkimVerified ? chalk.green('✓') : chalk.red('✗'),
28
- d.dmarcVerified ? chalk.green('✓') : chalk.red('✗'),
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
- spinner.stop();
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 spinner = ora('Adding domain…').start();
44
+ const sp = spinner(`Adding ${chalk.cyan(domain)}…`).start();
45
45
  try {
46
46
  const data = await client.post('/domains', { domain });
47
- spinner.stop();
47
+ sp.succeed(chalk.dim('Domain added'));
48
48
  if (opts.json) return json(data);
49
- success(`Domain ${domain} added.`);
50
- info('Add these DNS records to your domain:');
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 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}`);
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
- spinner.stop();
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 spinner = ora('Checking DNS…').start();
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
- success('All records verified! Domain is ready.');
76
+ sp.succeed(gradient(['#10b981', '#3b82f6'])('All records verified!'));
77
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.');
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
- spinner.stop();
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 for a domain')
101
+ .description('Show DNS records to configure')
99
102
  .option('--json', 'Output raw JSON')
100
103
  .action(async (id, opts) => {
101
- const spinner = ora('Loading…').start();
104
+ const sp = spinner('Loading DNS records…').start();
102
105
  try {
103
106
  const data = await client.get(`/domains/${id}`);
104
- spinner.stop();
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 = 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}`);
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
- spinner.stop();
119
+ sp.fail(chalk.red('Failed'));
116
120
  error(err.message);
117
121
  process.exit(1);
118
122
  }
@@ -1,7 +1,7 @@
1
1
  const { Command } = require('commander');
2
- const ora = require('ora');
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 spinner = ora('Fetching…').start();
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
- spinner.stop();
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
- spinner.stop();
38
- require('../output').error(err.message);
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 spinner = ora('Loading…').start();
48
+ const sp = spinner('Loading stats…').start();
49
49
  try {
50
50
  const data = await client.get('/emails/stats/summary');
51
- spinner.stop();
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', 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)}%` : '—'],
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
- spinner.stop();
69
- require('../output').error(err.message);
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 spinner = ora('Fetching…').start();
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
- spinner.stop();
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', e._id],
89
- ['To', e.toEmail],
90
- ['Subject', e.subject],
91
- ['Status', colorStatus(e.status)],
92
- ['From', e.fromEmail || '—'],
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
- spinner.stop();
98
- require('../output').error(err.message);
101
+ sp.fail(chalk.red('Failed'));
102
+ error(err.message);
99
103
  process.exit(1);
100
104
  }
101
105
  });