Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ dist/
# OS
.DS_Store
Thumbs.db
\~/.volta

# Environment
.env
Expand Down
19 changes: 13 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,19 @@ The official CLI for [Resend](https://resend.com).
Built for humans, AI agents, and CI/CD pipelines.

```
██████╗ ███████╗███████╗███████╗███╗ ██╗██████╗
██╔══██╗██╔════╝██╔════╝██╔════╝████╗ ██║██╔══██╗
██████╔╝█████╗ ███████╗█████╗ ██╔██╗ ██║██║ ██║
██╔══██╗██╔══╝ ╚════██║██╔══╝ ██║╚██╗██║██║ ██║
██║ ██║███████╗███████║███████╗██║ ╚████║██████╔╝
╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝╚═╝ ╚═══╝╚═════╝
+++++++++++++++++++++ +++++
+++++++++++++++++++++++ +++++
++++++++++ +++++
+++++++++ +++++ +++++++ +++++ ++++ +++ +++++
+++++++++ +++++++++++++ ++++++++++++++ +++++++++++++ ++++++++++++++++ ++++++++++++++++
+++++++++++ ++++++++++++++++ ++++++++++++++++ ++++++++++++++++ ++++++++++++++++++ ++++++++++++++++++
+++++++++++++++ +++++++ +++++++ +++++++ ++++++ ++++++ +++++++ +++++++ ++++++++ ++++++++ +++++++
+++++++++ +++++++++++++++++++ ++++++++++++++ +++++++++++++++++++ ++++++ ++++++ ++++++ +++++
++++++++++ +++++++++++++++++++ +++++++++++++++ +++++++++++++++++++ ++++++ +++++++++++++ +++++
++++++++++ ++++++ ++++++++++++ ++++++ ++++++ ++++++ ++++++ ++++++
+++++++++++ +++++++ +++++++ +++++++ ++++++ +++++++ +++++++ ++++++ ++++++ ++++++++ ++++++++
++++++++++ ++++++++++++++++ ++++++++++++++++ +++++++++++++++ ++++++ ++++++ ++++++++++++++++++
++++++++++ +++++++++++ +++++++++++ +++++++++++ ++++++ ++++++ +++++++++ +++++
```

## Install
Expand Down
6 changes: 4 additions & 2 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { webhooksCommand } from './commands/webhooks/index';
import { whoamiCommand } from './commands/whoami';
import { setupCliExitHandler } from './lib/cli-exit';
import { installCommandSuggestions } from './lib/command-suggestions';
import { printBannerPlain } from './lib/logo';
import { printBanner } from './lib/logo';
import { errorMessage, outputError } from './lib/output';
import { trackCommand } from './lib/telemetry';
import { checkForUpdates } from './lib/update-check';
Expand Down Expand Up @@ -108,11 +108,13 @@ ${pc.gray('Examples:')}
- Send an email

${pc.blue('$ resend emails send')}

${pc.dim('Run resend <command> --help for details on a specific command.')}
`,
)
.action(() => {
if (process.stdout.isTTY) {
printBannerPlain();
printBanner();
}
const opts = program.opts();
if (opts.apiKey) {
Expand Down
12 changes: 10 additions & 2 deletions src/commands/auth/login.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import * as p from '@clack/prompts';
import { Command } from '@commander-js/extra-typings';
import pc from 'picocolors';
import { Resend } from 'resend';
import { wordmark } from '../../lib/brand';
import { openInBrowser } from '../../lib/browser';
import type { GlobalOpts } from '../../lib/client';
import {
Expand All @@ -12,6 +14,7 @@ import {
validateProfileName,
} from '../../lib/config';
import { buildHelpText } from '../../lib/help-text';
import { printBanner } from '../../lib/logo';
import { errorMessage, outputError, outputResult } from '../../lib/output';
import { cancelAndExit, promptRenameIfInvalid } from '../../lib/prompts';
import { createSpinner } from '../../lib/spinner';
Expand Down Expand Up @@ -60,7 +63,10 @@ export const loginCommand = new Command('login')
);
}

p.intro('Resend Authentication');
if (process.stdout.isTTY) {
printBanner();
}
p.intro('Resend authentication');
p.log.info(
`Use a full access API key for complete CLI access.\n${SENDING_KEY_MESSAGE}`,
);
Expand Down Expand Up @@ -264,7 +270,9 @@ export const loginCommand = new Command('login')
: `in ${backend.name}`;
const msg = `API key stored for profile '${profileLabel}' ${storageInfo}`;
if (isInteractive()) {
p.outro(msg);
p.outro(
`${wordmark()} is ready — API key stored for profile '${pc.bold(profileLabel)}' ${storageInfo}`,
);
} else {
console.log(msg);
}
Expand Down
3 changes: 2 additions & 1 deletion src/commands/automations/runs/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { runGet } from '../../../lib/actions';
import type { GlobalOpts } from '../../../lib/client';
import { buildHelpText } from '../../../lib/help-text';
import { requireText } from '../../../lib/prompts';
import { runStatusIndicator } from './list';

export const getAutomationRunCommand = new Command('get')
.description('Retrieve details of a specific automation run')
Expand Down Expand Up @@ -52,7 +53,7 @@ export const getAutomationRunCommand = new Command('get')
resend.automations.runs.get({ automationId, runId }),
onInteractive: (r) => {
console.log(`Run: ${r.id}`);
console.log(`Status: ${r.status}`);
console.log(`Status: ${runStatusIndicator(r.status)}`);
if (r.started_at) {
console.log(`Started: ${r.started_at}`);
}
Expand Down
48 changes: 46 additions & 2 deletions src/commands/automations/runs/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,47 @@ import {
printPaginationHint,
} from '../../../lib/pagination';
import { pickId } from '../../../lib/prompts';
import { renderTable } from '../../../lib/table';
import { renderTable, type StatusTone } from '../../../lib/table';
import { isUnicodeSupported } from '../../../lib/tty';
import { automationPickerConfig } from '../utils';

// Status symbols generated via String.fromCodePoint() — never literal Unicode in
// source — to prevent UTF-8 → Latin-1 corruption when the npm package is bundled.
const CHECK = isUnicodeSupported ? String.fromCodePoint(0x2713) : 'v'; // ✓
const HOURGLASS = isUnicodeSupported ? String.fromCodePoint(0x23f3) : '~'; // ⏳
const CIRCLE = isUnicodeSupported ? String.fromCodePoint(0x25cb) : 'o'; // ○
const CROSS_MARK = isUnicodeSupported ? String.fromCodePoint(0x2717) : 'x'; // ✗

function runStatusTone(status: string): StatusTone {
switch (status) {
case 'completed':
return 'success';
case 'running':
return 'pending';
case 'failed':
return 'failure';
case 'cancelled':
return 'neutral';
default:
return 'neutral';
}
}

export function runStatusIndicator(status: string): string {
switch (status) {
case 'completed':
return `${CHECK} Completed`;
case 'running':
return `${HOURGLASS} Running`;
case 'failed':
return `${CROSS_MARK} Failed`;
case 'cancelled':
return `${CIRCLE} Cancelled`;
default:
return status;
}
}

export const listAutomationRunsCommand = new Command('list')
.alias('ls')
.description('List runs for an automation')
Expand Down Expand Up @@ -67,7 +105,7 @@ export const listAutomationRunsCommand = new Command('list')
onInteractive: (list) => {
const rows = list.data.map((r) => [
r.id,
r.status,
runStatusIndicator(r.status),
r.started_at ?? '-',
r.completed_at ?? '-',
]);
Expand All @@ -76,6 +114,12 @@ export const listAutomationRunsCommand = new Command('list')
['ID', 'Status', 'Started', 'Completed'],
rows,
'(no runs)',
{
statusColumn: {
index: 1,
tones: list.data.map((r) => runStatusTone(r.status)),
},
},
),
);
printPaginationHint(list, `automations runs list ${automationId}`, {
Expand Down
29 changes: 26 additions & 3 deletions src/commands/automations/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import type { PickerConfig } from '../../lib/prompts';
import { renderTable } from '../../lib/table';
import { renderTable, type StatusTone } from '../../lib/table';
import { isUnicodeSupported } from '../../lib/tty';

// Status symbols generated via String.fromCodePoint() — never literal Unicode in
// source — to prevent UTF-8 → Latin-1 corruption when the npm package is bundled.
const CHECK = isUnicodeSupported ? String.fromCodePoint(0x2713) : 'v'; // ✓
const CIRCLE = isUnicodeSupported ? String.fromCodePoint(0x25cb) : 'o'; // ○

export function renderAutomationsTable(
automations: Array<{
Expand All @@ -19,20 +25,37 @@ export function renderAutomationsTable(
['Name', 'Status', 'Created', 'ID'],
rows,
'(no automations)',
{
statusColumn: {
index: 1,
tones: automations.map((a) => automationStatusTone(a.status)),
},
},
);
}

export function statusIndicator(status: string): string {
switch (status) {
case 'enabled':
return '✓ Enabled';
return `${CHECK} Enabled`;
case 'disabled':
return '○ Disabled';
return `${CIRCLE} Disabled`;
default:
return status;
}
}

function automationStatusTone(status: string): StatusTone {
switch (status) {
case 'enabled':
return 'success';
case 'disabled':
return 'neutral';
default:
return 'neutral';
}
}

export const automationPickerConfig: PickerConfig<{
id: string;
name: string;
Expand Down
34 changes: 30 additions & 4 deletions src/commands/broadcasts/utils.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,39 @@
import type { PickerConfig } from '../../lib/prompts';
import { renderTable } from '../../lib/table';
import { renderTable, type StatusTone } from '../../lib/table';
import { isUnicodeSupported } from '../../lib/tty';

// Status symbols generated via String.fromCodePoint() — never literal Unicode in
// source — to prevent UTF-8 → Latin-1 corruption when the npm package is bundled.
const CHECK = isUnicodeSupported ? String.fromCodePoint(0x2713) : 'v'; // ✓
const HOURGLASS = isUnicodeSupported ? String.fromCodePoint(0x23f3) : '~'; // ⏳
const CIRCLE = isUnicodeSupported ? String.fromCodePoint(0x25cb) : 'o'; // ○

export function broadcastStatusIndicator(status: string): string {
switch (status) {
case 'draft':
return '○ Draft';
return `${CIRCLE} Draft`;
case 'queued':
return '⏳ Queued';
return `${HOURGLASS} Queued`;
case 'sent':
return '✓ Sent';
return `${CHECK} Sent`;
default:
return status;
}
}

function broadcastStatusTone(status: string): StatusTone {
switch (status) {
case 'sent':
return 'success';
case 'queued':
return 'pending';
case 'draft':
return 'neutral';
default:
return 'neutral';
}
}

export const broadcastPickerConfig: PickerConfig<{
id: string;
name: string | null;
Expand Down Expand Up @@ -59,5 +79,11 @@ export function renderBroadcastsTable(
['Name', 'Status', 'Created', 'ID'],
rows,
'(no broadcasts)',
{
statusColumn: {
index: 1,
tones: broadcasts.map((b) => broadcastStatusTone(b.status)),
},
},
);
}
4 changes: 2 additions & 2 deletions src/commands/contacts/imports/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { runGet } from '../../../lib/actions';
import type { GlobalOpts } from '../../../lib/client';
import { buildHelpText } from '../../../lib/help-text';
import { pickId } from '../../../lib/prompts';
import { contactImportPickerConfig } from './utils';
import { contactImportPickerConfig, importStatusIndicator } from './utils';

export const getContactImportCommand = new Command('get')
.description('Retrieve a contact import by ID')
Expand All @@ -28,7 +28,7 @@ export const getContactImportCommand = new Command('get')
loading: 'Fetching contact import...',
sdkCall: (resend) => resend.contacts.imports.get(id),
onInteractive: (imp) => {
console.log(`${imp.id} - ${imp.status}`);
console.log(`${imp.id} - ${importStatusIndicator(imp.status)}`);
console.log(`Created: ${imp.created_at}`);
if (imp.completed_at) {
console.log(`Completed: ${imp.completed_at}`);
Expand Down
46 changes: 44 additions & 2 deletions src/commands/contacts/imports/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,49 @@ import type { ContactImport, ContactImportColumnMap } from 'resend';
import type { GlobalOpts } from '../../../lib/client';
import { outputError } from '../../../lib/output';
import type { PickerConfig } from '../../../lib/prompts';
import { renderTable } from '../../../lib/table';
import { renderTable, type StatusTone } from '../../../lib/table';
import { isUnicodeSupported } from '../../../lib/tty';

// ─── Table renderer ────────────────────────────────────────────────────────

// Status symbols generated via String.fromCodePoint() — never literal Unicode in
// source — to prevent UTF-8 → Latin-1 corruption when the npm package is bundled.
const CHECK = isUnicodeSupported ? String.fromCodePoint(0x2713) : 'v'; // ✓
const HOURGLASS = isUnicodeSupported ? String.fromCodePoint(0x23f3) : '~'; // ⏳
const CROSS_MARK = isUnicodeSupported ? String.fromCodePoint(0x2717) : 'x'; // ✗

function importStatusTone(status: string): StatusTone {
switch (status) {
case 'completed':
return 'success';
case 'queued':
case 'in_progress':
return 'pending';
case 'failed':
return 'failure';
default:
return 'neutral';
}
}

export function importStatusIndicator(status: string): string {
switch (status) {
case 'completed':
return `${CHECK} Completed`;
case 'queued':
return `${HOURGLASS} Queued`;
case 'in_progress':
return `${HOURGLASS} In progress`;
case 'failed':
return `${CROSS_MARK} Failed`;
default:
return status;
}
}

export function renderContactImportsTable(imports: ContactImport[]): string {
const rows = imports.map((imp) => [
imp.status,
importStatusIndicator(imp.status),
String(imp.counts.total),
String(imp.counts.created),
String(imp.counts.updated),
Expand All @@ -30,6 +66,12 @@ export function renderContactImportsTable(imports: ContactImport[]): string {
],
rows,
'(no contact imports)',
{
statusColumn: {
index: 0,
tones: imports.map((imp) => importStatusTone(imp.status)),
},
},
);
}

Expand Down
Loading
Loading