diff --git a/src/cli.ts b/src/cli.ts index 910ef63..cff0caa 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -20,6 +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 { printBannerPlain } from './lib/logo'; import { errorMessage, outputError } from './lib/output'; import { trackCommand } from './lib/telemetry'; import { checkForUpdates } from './lib/update-check'; @@ -116,6 +117,9 @@ ${pc.gray('Examples:')} `, ) .action(() => { + if (process.stdout.isTTY) { + printBannerPlain(); + } const opts = program.opts(); if (opts.apiKey) { outputError( diff --git a/src/lib/logo.ts b/src/lib/logo.ts new file mode 100644 index 0000000..ee96ee9 --- /dev/null +++ b/src/lib/logo.ts @@ -0,0 +1,16 @@ +const LOGO_LINES = [ + ' ██████╗ ███████╗███████╗███████╗███╗ ██╗██████╗ ', + ' ██╔══██╗██╔════╝██╔════╝██╔════╝████╗ ██║██╔══██╗', + ' ██████╔╝█████╗ ███████╗█████╗ ██╔██╗ ██║██║ ██║', + ' ██╔══██╗██╔══╝ ╚════██║██╔══╝ ██║╚██╗██║██║ ██║', + ' ██║ ██║███████╗███████║███████╗██║ ╚████║██████╔╝', + ' ╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝╚═╝ ╚═══╝╚═════╝ ', +]; + +export function printBannerPlain(): void { + process.stdout.write('\n'); + for (const line of LOGO_LINES) { + process.stdout.write(`${line}\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..cd8b726 --- /dev/null +++ b/tests/lib/logo.test.ts @@ -0,0 +1,37 @@ +import { + afterEach, + describe, + expect, + type MockInstance, + test, + vi, +} from 'vitest'; +import { printBannerPlain } from '../../src/lib/logo'; + +describe('printBannerPlain', () => { + let writeSpy: MockInstance; + + afterEach(() => { + writeSpy?.mockRestore(); + }); + + test('writes ASCII logo 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; + }); + + printBannerPlain(); + + const out = chunks.join(''); + expect(out).toContain('██████╗'); + expect(out).toContain('█'); + }); +}); diff --git a/tests/welcome.test.ts b/tests/welcome.test.ts new file mode 100644 index 0000000..a64e22d --- /dev/null +++ b/tests/welcome.test.ts @@ -0,0 +1,35 @@ +import { type ExecFileSyncOptions, 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 shows help when invoked with no arguments', () => { + 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).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('██████╗'); + }); +});