From 08c4d20a80a3acb1260d8e7b5044e22dbc751cca Mon Sep 17 00:00:00 2001 From: Shubhdeep Chhabra Date: Fri, 13 Mar 2026 23:19:18 +0530 Subject: [PATCH 1/4] feat: add welcome message and update check on no arguments in CLI --- src/cli.ts | 41 ++++++++++++++++++-------- src/lib/logo.ts | 67 ++++++++++++++++++++++++++++++++++++++++++ tests/lib/logo.test.ts | 61 ++++++++++++++++++++++++++++++++++++++ tests/welcome.test.ts | 34 +++++++++++++++++++++ 4 files changed, 190 insertions(+), 13 deletions(-) create mode 100644 src/lib/logo.ts create mode 100644 tests/lib/logo.test.ts create mode 100644 tests/welcome.test.ts diff --git a/src/cli.ts b/src/cli.ts index f8c998a..4a175c1 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -19,6 +19,7 @@ import { topicsCommand } from './commands/topics/index'; import { updateCommand } from './commands/update'; import { webhooksCommand } from './commands/webhooks/index'; import { whoamiCommand } from './commands/whoami'; +import { printWelcome } from './lib/logo'; import { errorMessage, outputError } from './lib/output'; import { checkForUpdates } from './lib/update-check'; import { PACKAGE_NAME, VERSION } from './lib/version'; @@ -100,19 +101,33 @@ if (teamOption) { teamOption.hidden = true; } -program - .parseAsync() - .then(() => { - // Skip the background update notice when the user explicitly ran `update` - const ran = program.args[0]; - if (ran === 'update') { - return; - } - return checkForUpdates().catch(() => {}); - }) - .catch((err) => { +const args = process.argv.slice(2); +if (args.length === 0) { + (async () => { + printWelcome(VERSION); + await checkForUpdates(); + process.exit(0); + })().catch((err) => { outputError({ - message: errorMessage(err, 'An unexpected error occurred'), - code: 'unexpected_error', + message: errorMessage(err, 'Failed to show welcome'), + code: 'welcome_error', }); }); +} else { + program + .parseAsync() + .then(() => { + // Skip the background update notice when the user explicitly ran `update` + const ran = program.args[0]; + if (ran === 'update') { + return; + } + return checkForUpdates().catch(() => {}); + }) + .catch((err) => { + outputError({ + message: errorMessage(err, 'An unexpected error occurred'), + code: 'unexpected_error', + }); + }); +} diff --git a/src/lib/logo.ts b/src/lib/logo.ts new file mode 100644 index 0000000..952e5cc --- /dev/null +++ b/src/lib/logo.ts @@ -0,0 +1,67 @@ +import pc from 'picocolors'; + +const LOGO_LINES = [ + ' ██████╗ ███████╗███████╗███████╗███╗ ██╗██████╗ ', + ' ██╔══██╗██╔════╝██╔════╝██╔════╝████╗ ██║██╔══██╗', + ' ██████╔╝█████╗ ███████╗█████╗ ██╔██╗ ██║██║ ██║', + ' ██╔══██╗██╔══╝ ╚════██║██╔══╝ ██║╚██╗██║██║ ██║', + ' ██║ ██║███████╗███████║███████╗██║ ╚████║██████╔╝', + ' ╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝╚═╝ ╚═══╝╚═════╝ ', +]; + +const LOGO_COLORS = [ + (t: string) => pc.white(pc.bold(t)), + (t: string) => pc.white(t), + (t: string) => pc.gray(pc.bold(t)), + (t: string) => pc.gray(t), + (t: string) => pc.dim(pc.gray(t)), + (t: string) => pc.dim(pc.gray(t)), +]; + +export function printWelcome(version: string): void { + process.stdout.write('\n'); + for (let i = 0; i < LOGO_LINES.length; i++) { + const line = LOGO_LINES[i]; + const color = LOGO_COLORS[i] ?? ((t: string) => t); + process.stdout.write(` ${color(line)}\n`); + } + process.stdout.write('\n'); + const cmd = (c: string) => pc.white(c); + const dim = (t: string) => pc.dim(t); + const CMD_WIDTH = 42; + + process.stdout.write( + ` ${dim(`v${version}`)} ${dim('—')} ${pc.white('Power your emails with code')}\n`, + ); + process.stdout.write('\n'); + + const hints: [string, string][] = [ + ['resend --help', 'Display help and commands'], + ['resend login | logout', 'Authenticate with Resend'], + ['resend auth list | switch | remove', 'Manage profiles'], + ['resend emails send | batch | receiving', 'Send and manage emails'], + ['resend domains list | verify | create', 'Sending and receiving domains'], + ['resend contacts list | create', 'Contacts and segments'], + ['resend broadcasts list | send', 'Bulk email to segments'], + ['resend webhooks list | create', 'Event notifications'], + ['resend templates list | create', 'Reusable email templates'], + [ + 'resend whoami | doctor | open | update', + 'Status, health, dashboard, upgrade', + ], + ]; + + for (const [command, description] of hints) { + const pad = ' '.repeat(Math.max(0, CMD_WIDTH - command.length)); + process.stdout.write( + ` ${dim('$')} ${cmd(command)}${pad} ${dim(description)}\n`, + ); + } + + process.stdout.write('\n'); + process.stdout.write(` ${dim('try:')} ${cmd('resend login')}\n`); + process.stdout.write( + ` ${dim('Learn more at')} ${dim('https://resend.com/docs')}\n`, + ); + process.stdout.write('\n'); +} diff --git a/tests/lib/logo.test.ts b/tests/lib/logo.test.ts new file mode 100644 index 0000000..59363d2 --- /dev/null +++ b/tests/lib/logo.test.ts @@ -0,0 +1,61 @@ +import { + afterEach, + describe, + expect, + type MockInstance, + test, + vi, +} from 'vitest'; +import { printWelcome } from '../../src/lib/logo'; + +describe('printWelcome', () => { + let writeSpy: MockInstance; + + afterEach(() => { + writeSpy?.mockRestore(); + }); + + test('writes ASCII logo and tagline to stdout', () => { + const chunks: string[] = []; + writeSpy = vi + .spyOn(process.stdout, 'write') + .mockImplementation((chunk: unknown) => { + chunks.push( + typeof chunk === 'string' + ? chunk + : new TextDecoder().decode(chunk as Uint8Array), + ); + return true; + }); + + printWelcome('1.2.3'); + + const out = chunks.join(''); + expect(out).toContain('██████╗'); + expect(out).toContain('█'); + expect(out).toContain('v1.2.3'); + expect(out).toContain('Power your emails with code'); + }); + + test('includes command hints and try/login', () => { + const chunks: string[] = []; + writeSpy = vi + .spyOn(process.stdout, 'write') + .mockImplementation((chunk: unknown) => { + chunks.push( + typeof chunk === 'string' + ? chunk + : new TextDecoder().decode(chunk as Uint8Array), + ); + return true; + }); + + printWelcome('0.0.1'); + + const out = chunks.join(''); + expect(out).toContain('resend --help'); + expect(out).toContain('resend login'); + expect(out).toContain('try:'); + expect(out).toContain('resend.com/docs'); + }); +}); diff --git a/tests/welcome.test.ts b/tests/welcome.test.ts new file mode 100644 index 0000000..fbe3c28 --- /dev/null +++ b/tests/welcome.test.ts @@ -0,0 +1,34 @@ +import { execFileSync } from 'node:child_process'; +import { resolve } from 'node:path'; +import { describe, expect, test } from 'vitest'; + +const CLI = resolve(import.meta.dirname, '../src/cli.ts'); + +const noUpdateEnv = { + ...process.env, + RESEND_NO_UPDATE_NOTIFIER: '1', + NO_COLOR: '1', +}; + +describe('no-args welcome', () => { + test('exits 0 and stdout contains tagline and command hints', () => { + let stdout: string; + let exitCode: number; + try { + stdout = execFileSync('npx', ['tsx', CLI], { + encoding: 'utf-8', + timeout: 10_000, + env: noUpdateEnv, + }); + exitCode = 0; + } catch (err) { + const e = err as { stdout?: string; stderr?: string; status?: number }; + stdout = (e.stdout ?? '').trim(); + exitCode = e.status ?? 1; + } + expect(exitCode).toBe(0); + expect(stdout).toContain('Power your emails with code'); + expect(stdout).toContain('resend --help'); + expect(stdout).toContain('resend login'); + }); +}); From e8ef0b6c82923a8c37cf47c4150e98790de65ccf Mon Sep 17 00:00:00 2001 From: Shubhdeep Chhabra Date: Sat, 14 Mar 2026 00:08:02 +0530 Subject: [PATCH 2/4] refactor: remove unnecessary process.exit in CLI and enhance test exec options --- src/cli.ts | 1 - tests/welcome.test.ts | 15 +++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 4a175c1..56fd2a1 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -106,7 +106,6 @@ if (args.length === 0) { (async () => { printWelcome(VERSION); await checkForUpdates(); - process.exit(0); })().catch((err) => { outputError({ message: errorMessage(err, 'Failed to show welcome'), diff --git a/tests/welcome.test.ts b/tests/welcome.test.ts index fbe3c28..2ec40a6 100644 --- a/tests/welcome.test.ts +++ b/tests/welcome.test.ts @@ -1,4 +1,4 @@ -import { execFileSync } from 'node:child_process'; +import { type ExecFileSyncOptions, execFileSync } from 'node:child_process'; import { resolve } from 'node:path'; import { describe, expect, test } from 'vitest'; @@ -14,12 +14,15 @@ describe('no-args welcome', () => { test('exits 0 and stdout contains tagline and command hints', () => { let stdout: string; let exitCode: number; + const execOptions: ExecFileSyncOptions = { + encoding: 'utf-8', + timeout: 10_000, + env: noUpdateEnv, + ...(process.platform === 'win32' ? { shell: true } : {}), + }; try { - stdout = execFileSync('npx', ['tsx', CLI], { - encoding: 'utf-8', - timeout: 10_000, - env: noUpdateEnv, - }); + const result = execFileSync('npx', ['tsx', CLI], execOptions); + stdout = Buffer.isBuffer(result) ? result.toString('utf8') : result; exitCode = 0; } catch (err) { const e = err as { stdout?: string; stderr?: string; status?: number }; From f7bc25e0583a8387e29308a9328f9ff3a3a49b88 Mon Sep 17 00:00:00 2001 From: Felipe Freitag Date: Fri, 20 Mar 2026 14:10:04 -0300 Subject: [PATCH 3/4] refactor: simplify welcome banner to plain ASCII without colors or hints --- src/cli.ts | 4 +-- src/lib/logo.ts | 69 ++++++------------------------------------ tests/lib/logo.test.ts | 32 +++----------------- tests/welcome.test.ts | 19 ++---------- 4 files changed, 18 insertions(+), 106 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 211623e..73875bb 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -20,7 +20,7 @@ import { updateCommand } from './commands/update'; import { webhooksCommand } from './commands/webhooks/index'; import { whoamiCommand } from './commands/whoami'; import { setupCliExitHandler } from './lib/cli-exit'; -import { printWelcome } from './lib/logo'; +import { printBannerPlain } from './lib/logo'; import { errorMessage, outputError } from './lib/output'; import { trackCommand } from './lib/telemetry'; import { checkForUpdates } from './lib/update-check'; @@ -117,7 +117,7 @@ ${pc.gray('Examples:')} `, ) .action(() => { - printWelcome(VERSION); + printBannerPlain(); const opts = program.opts(); if (opts.apiKey) { outputError( diff --git a/src/lib/logo.ts b/src/lib/logo.ts index 952e5cc..ee96ee9 100644 --- a/src/lib/logo.ts +++ b/src/lib/logo.ts @@ -1,67 +1,16 @@ -import pc from 'picocolors'; - const LOGO_LINES = [ - ' ██████╗ ███████╗███████╗███████╗███╗ ██╗██████╗ ', - ' ██╔══██╗██╔════╝██╔════╝██╔════╝████╗ ██║██╔══██╗', - ' ██████╔╝█████╗ ███████╗█████╗ ██╔██╗ ██║██║ ██║', - ' ██╔══██╗██╔══╝ ╚════██║██╔══╝ ██║╚██╗██║██║ ██║', - ' ██║ ██║███████╗███████║███████╗██║ ╚████║██████╔╝', - ' ╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝╚═╝ ╚═══╝╚═════╝ ', -]; - -const LOGO_COLORS = [ - (t: string) => pc.white(pc.bold(t)), - (t: string) => pc.white(t), - (t: string) => pc.gray(pc.bold(t)), - (t: string) => pc.gray(t), - (t: string) => pc.dim(pc.gray(t)), - (t: string) => pc.dim(pc.gray(t)), + ' ██████╗ ███████╗███████╗███████╗███╗ ██╗██████╗ ', + ' ██╔══██╗██╔════╝██╔════╝██╔════╝████╗ ██║██╔══██╗', + ' ██████╔╝█████╗ ███████╗█████╗ ██╔██╗ ██║██║ ██║', + ' ██╔══██╗██╔══╝ ╚════██║██╔══╝ ██║╚██╗██║██║ ██║', + ' ██║ ██║███████╗███████║███████╗██║ ╚████║██████╔╝', + ' ╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝╚═╝ ╚═══╝╚═════╝ ', ]; -export function printWelcome(version: string): void { +export function printBannerPlain(): void { process.stdout.write('\n'); - for (let i = 0; i < LOGO_LINES.length; i++) { - const line = LOGO_LINES[i]; - const color = LOGO_COLORS[i] ?? ((t: string) => t); - process.stdout.write(` ${color(line)}\n`); + for (const line of LOGO_LINES) { + process.stdout.write(`${line}\n`); } process.stdout.write('\n'); - const cmd = (c: string) => pc.white(c); - const dim = (t: string) => pc.dim(t); - const CMD_WIDTH = 42; - - process.stdout.write( - ` ${dim(`v${version}`)} ${dim('—')} ${pc.white('Power your emails with code')}\n`, - ); - process.stdout.write('\n'); - - const hints: [string, string][] = [ - ['resend --help', 'Display help and commands'], - ['resend login | logout', 'Authenticate with Resend'], - ['resend auth list | switch | remove', 'Manage profiles'], - ['resend emails send | batch | receiving', 'Send and manage emails'], - ['resend domains list | verify | create', 'Sending and receiving domains'], - ['resend contacts list | create', 'Contacts and segments'], - ['resend broadcasts list | send', 'Bulk email to segments'], - ['resend webhooks list | create', 'Event notifications'], - ['resend templates list | create', 'Reusable email templates'], - [ - 'resend whoami | doctor | open | update', - 'Status, health, dashboard, upgrade', - ], - ]; - - for (const [command, description] of hints) { - const pad = ' '.repeat(Math.max(0, CMD_WIDTH - command.length)); - process.stdout.write( - ` ${dim('$')} ${cmd(command)}${pad} ${dim(description)}\n`, - ); - } - - process.stdout.write('\n'); - process.stdout.write(` ${dim('try:')} ${cmd('resend login')}\n`); - process.stdout.write( - ` ${dim('Learn more at')} ${dim('https://resend.com/docs')}\n`, - ); - process.stdout.write('\n'); } diff --git a/tests/lib/logo.test.ts b/tests/lib/logo.test.ts index 59363d2..cd8b726 100644 --- a/tests/lib/logo.test.ts +++ b/tests/lib/logo.test.ts @@ -6,16 +6,16 @@ import { test, vi, } from 'vitest'; -import { printWelcome } from '../../src/lib/logo'; +import { printBannerPlain } from '../../src/lib/logo'; -describe('printWelcome', () => { +describe('printBannerPlain', () => { let writeSpy: MockInstance; afterEach(() => { writeSpy?.mockRestore(); }); - test('writes ASCII logo and tagline to stdout', () => { + test('writes ASCII logo to stdout', () => { const chunks: string[] = []; writeSpy = vi .spyOn(process.stdout, 'write') @@ -28,34 +28,10 @@ describe('printWelcome', () => { return true; }); - printWelcome('1.2.3'); + printBannerPlain(); const out = chunks.join(''); expect(out).toContain('██████╗'); expect(out).toContain('█'); - expect(out).toContain('v1.2.3'); - expect(out).toContain('Power your emails with code'); - }); - - test('includes command hints and try/login', () => { - const chunks: string[] = []; - writeSpy = vi - .spyOn(process.stdout, 'write') - .mockImplementation((chunk: unknown) => { - chunks.push( - typeof chunk === 'string' - ? chunk - : new TextDecoder().decode(chunk as Uint8Array), - ); - return true; - }); - - printWelcome('0.0.1'); - - const out = chunks.join(''); - expect(out).toContain('resend --help'); - expect(out).toContain('resend login'); - expect(out).toContain('try:'); - expect(out).toContain('resend.com/docs'); }); }); diff --git a/tests/welcome.test.ts b/tests/welcome.test.ts index 2ec40a6..b56f9eb 100644 --- a/tests/welcome.test.ts +++ b/tests/welcome.test.ts @@ -11,27 +11,14 @@ const noUpdateEnv = { }; describe('no-args welcome', () => { - test('exits 0 and stdout contains tagline and command hints', () => { - let stdout: string; - let exitCode: number; + test('exits 0 and stdout contains ASCII banner', () => { const execOptions: ExecFileSyncOptions = { encoding: 'utf-8', timeout: 10_000, env: noUpdateEnv, ...(process.platform === 'win32' ? { shell: true } : {}), }; - try { - const result = execFileSync('npx', ['tsx', CLI], execOptions); - stdout = Buffer.isBuffer(result) ? result.toString('utf8') : result; - exitCode = 0; - } catch (err) { - const e = err as { stdout?: string; stderr?: string; status?: number }; - stdout = (e.stdout ?? '').trim(); - exitCode = e.status ?? 1; - } - expect(exitCode).toBe(0); - expect(stdout).toContain('Power your emails with code'); - expect(stdout).toContain('resend --help'); - expect(stdout).toContain('resend login'); + const stdout = execFileSync('npx', ['tsx', CLI], execOptions) as string; + expect(stdout).toContain('██████╗'); }); }); From 34676ca2365b3dd7e26c6edb005bd73373db8595 Mon Sep 17 00:00:00 2001 From: Felipe Freitag Date: Fri, 20 Mar 2026 14:13:55 -0300 Subject: [PATCH 4/4] feat: skip ASCII banner when stdout is not a TTY --- src/cli.ts | 4 +++- tests/welcome.test.ts | 15 +++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 73875bb..cff0caa 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -117,7 +117,9 @@ ${pc.gray('Examples:')} `, ) .action(() => { - printBannerPlain(); + if (process.stdout.isTTY) { + printBannerPlain(); + } const opts = program.opts(); if (opts.apiKey) { outputError( diff --git a/tests/welcome.test.ts b/tests/welcome.test.ts index b56f9eb..a64e22d 100644 --- a/tests/welcome.test.ts +++ b/tests/welcome.test.ts @@ -11,7 +11,7 @@ const noUpdateEnv = { }; describe('no-args welcome', () => { - test('exits 0 and stdout contains ASCII banner', () => { + test('exits 0 and shows help when invoked with no arguments', () => { const execOptions: ExecFileSyncOptions = { encoding: 'utf-8', timeout: 10_000, @@ -19,6 +19,17 @@ describe('no-args welcome', () => { ...(process.platform === 'win32' ? { shell: true } : {}), }; const stdout = execFileSync('npx', ['tsx', CLI], execOptions) as string; - expect(stdout).toContain('██████╗'); + expect(stdout).toContain('Usage: resend'); + }); + + test('skips banner when stdout is not a TTY', () => { + const execOptions: ExecFileSyncOptions = { + encoding: 'utf-8', + timeout: 10_000, + env: noUpdateEnv, + ...(process.platform === 'win32' ? { shell: true } : {}), + }; + const stdout = execFileSync('npx', ['tsx', CLI], execOptions) as string; + expect(stdout).not.toContain('██████╗'); }); });