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 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') + ' to see available commands.');
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
- program.parse(process.argv);
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
- program.help();
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 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
  });
@@ -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 spinner = ora('Fetching…').start();
14
+ const sp = spinner('Fetching API keys…').start();
15
15
  try {
16
16
  const data = await client.get('/user/keys');
17
- spinner.stop();
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 found.');
20
+ if (!keys.length) return info('No API keys. Run: ' + chalk.cyan('sendcraft keys create <name>'));
21
21
  table(
22
- ['ID', 'Name', 'Key (masked)', 'Permissions', 'Last Used', 'Created'],
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 || 'full_access',
28
- k.lastUsedAt ? new Date(k.lastUsedAt).toLocaleDateString() : 'Never',
29
- k.createdAt ? new Date(k.createdAt).toLocaleDateString() : '—',
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
- spinner.stop();
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 <domains>', 'Comma-separated allowed sender 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 spinner = ora('Creating…').start();
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
- spinner.stop();
54
+ sp.succeed(chalk.dim('Created'));
55
55
  if (opts.json) return json(data);
56
+
56
57
  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}`);
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(`Allowed domains: ${key.allowedDomains.join(', ')}`);
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
- spinner.stop();
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('Revoke an API key permanently')
77
+ .description('Permanently revoke an API key')
74
78
  .option('--json', 'Output raw JSON')
75
79
  .action(async (keyId, opts) => {
76
- const spinner = ora('Revoking…').start();
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
- spinner.stop();
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
- spinner.stop();
87
+ sp.fail(chalk.red('Failed'));
84
88
  error(err.message);
85
89
  process.exit(1);
86
90
  }