resend-cli 1.2.1 → 1.2.2
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/biome.json +1 -1
- package/bun.lock +0 -3
- package/package.json +2 -3
- package/src/cli.ts +11 -1
- package/src/commands/auth/login.ts +35 -8
- package/src/commands/doctor.ts +33 -115
- package/src/commands/teams/remove.ts +5 -2
- package/src/commands/teams/switch.ts +3 -0
- package/src/lib/config.ts +37 -31
- package/src/lib/spinner.ts +17 -10
- package/src/lib/update-check.ts +172 -0
- package/tests/commands/auth/login.test.ts +37 -0
- package/tests/lib/config.test.ts +38 -7
- package/tests/lib/update-check.test.ts +169 -0
- package/.claude/worktrees/emails-list/.claude/settings.local.json +0 -5
- package/.claude/worktrees/emails-list/.github/scripts/pr-title-check.js +0 -34
- package/.claude/worktrees/emails-list/.github/workflows/ci.yml +0 -32
- package/.claude/worktrees/emails-list/.github/workflows/pr-title-check.yml +0 -13
- package/.claude/worktrees/emails-list/.github/workflows/release.yml +0 -93
- package/.claude/worktrees/emails-list/CHANGELOG.md +0 -31
- package/.claude/worktrees/emails-list/LICENSE +0 -21
- package/.claude/worktrees/emails-list/README.md +0 -424
- package/.claude/worktrees/emails-list/biome.json +0 -36
- package/.claude/worktrees/emails-list/bun.lock +0 -76
- package/.claude/worktrees/emails-list/bunfig.toml +0 -2
- package/.claude/worktrees/emails-list/install.ps1 +0 -140
- package/.claude/worktrees/emails-list/install.sh +0 -301
- package/.claude/worktrees/emails-list/package.json +0 -43
- package/.claude/worktrees/emails-list/renovate.json +0 -6
- package/.claude/worktrees/emails-list/src/cli.ts +0 -74
- package/.claude/worktrees/emails-list/src/commands/api-keys/create.ts +0 -114
- package/.claude/worktrees/emails-list/src/commands/api-keys/delete.ts +0 -47
- package/.claude/worktrees/emails-list/src/commands/api-keys/index.ts +0 -26
- package/.claude/worktrees/emails-list/src/commands/api-keys/list.ts +0 -35
- package/.claude/worktrees/emails-list/src/commands/api-keys/utils.ts +0 -8
- package/.claude/worktrees/emails-list/src/commands/auth/index.ts +0 -20
- package/.claude/worktrees/emails-list/src/commands/auth/login.ts +0 -207
- package/.claude/worktrees/emails-list/src/commands/auth/logout.ts +0 -105
- package/.claude/worktrees/emails-list/src/commands/broadcasts/create.ts +0 -196
- package/.claude/worktrees/emails-list/src/commands/broadcasts/delete.ts +0 -46
- package/.claude/worktrees/emails-list/src/commands/broadcasts/get.ts +0 -59
- package/.claude/worktrees/emails-list/src/commands/broadcasts/index.ts +0 -43
- package/.claude/worktrees/emails-list/src/commands/broadcasts/list.ts +0 -60
- package/.claude/worktrees/emails-list/src/commands/broadcasts/send.ts +0 -56
- package/.claude/worktrees/emails-list/src/commands/broadcasts/update.ts +0 -95
- package/.claude/worktrees/emails-list/src/commands/broadcasts/utils.ts +0 -35
- package/.claude/worktrees/emails-list/src/commands/contact-properties/create.ts +0 -118
- package/.claude/worktrees/emails-list/src/commands/contact-properties/delete.ts +0 -48
- package/.claude/worktrees/emails-list/src/commands/contact-properties/get.ts +0 -46
- package/.claude/worktrees/emails-list/src/commands/contact-properties/index.ts +0 -48
- package/.claude/worktrees/emails-list/src/commands/contact-properties/list.ts +0 -68
- package/.claude/worktrees/emails-list/src/commands/contact-properties/update.ts +0 -88
- package/.claude/worktrees/emails-list/src/commands/contact-properties/utils.ts +0 -17
- package/.claude/worktrees/emails-list/src/commands/contacts/add-segment.ts +0 -78
- package/.claude/worktrees/emails-list/src/commands/contacts/create.ts +0 -122
- package/.claude/worktrees/emails-list/src/commands/contacts/delete.ts +0 -49
- package/.claude/worktrees/emails-list/src/commands/contacts/get.ts +0 -53
- package/.claude/worktrees/emails-list/src/commands/contacts/index.ts +0 -58
- package/.claude/worktrees/emails-list/src/commands/contacts/list.ts +0 -57
- package/.claude/worktrees/emails-list/src/commands/contacts/remove-segment.ts +0 -48
- package/.claude/worktrees/emails-list/src/commands/contacts/segments.ts +0 -39
- package/.claude/worktrees/emails-list/src/commands/contacts/topics.ts +0 -45
- package/.claude/worktrees/emails-list/src/commands/contacts/update-topics.ts +0 -90
- package/.claude/worktrees/emails-list/src/commands/contacts/update.ts +0 -77
- package/.claude/worktrees/emails-list/src/commands/contacts/utils.ts +0 -119
- package/.claude/worktrees/emails-list/src/commands/doctor.ts +0 -298
- package/.claude/worktrees/emails-list/src/commands/domains/create.ts +0 -83
- package/.claude/worktrees/emails-list/src/commands/domains/delete.ts +0 -42
- package/.claude/worktrees/emails-list/src/commands/domains/get.ts +0 -47
- package/.claude/worktrees/emails-list/src/commands/domains/index.ts +0 -35
- package/.claude/worktrees/emails-list/src/commands/domains/list.ts +0 -53
- package/.claude/worktrees/emails-list/src/commands/domains/update.ts +0 -75
- package/.claude/worktrees/emails-list/src/commands/domains/utils.ts +0 -44
- package/.claude/worktrees/emails-list/src/commands/domains/verify.ts +0 -38
- package/.claude/worktrees/emails-list/src/commands/emails/batch.ts +0 -140
- package/.claude/worktrees/emails-list/src/commands/emails/index.ts +0 -28
- package/.claude/worktrees/emails-list/src/commands/emails/list.ts +0 -73
- package/.claude/worktrees/emails-list/src/commands/emails/receiving/attachment.ts +0 -55
- package/.claude/worktrees/emails-list/src/commands/emails/receiving/attachments.ts +0 -68
- package/.claude/worktrees/emails-list/src/commands/emails/receiving/get.ts +0 -58
- package/.claude/worktrees/emails-list/src/commands/emails/receiving/index.ts +0 -28
- package/.claude/worktrees/emails-list/src/commands/emails/receiving/list.ts +0 -59
- package/.claude/worktrees/emails-list/src/commands/emails/receiving/utils.ts +0 -38
- package/.claude/worktrees/emails-list/src/commands/emails/send.ts +0 -189
- package/.claude/worktrees/emails-list/src/commands/open.ts +0 -24
- package/.claude/worktrees/emails-list/src/commands/segments/create.ts +0 -50
- package/.claude/worktrees/emails-list/src/commands/segments/delete.ts +0 -47
- package/.claude/worktrees/emails-list/src/commands/segments/get.ts +0 -38
- package/.claude/worktrees/emails-list/src/commands/segments/index.ts +0 -36
- package/.claude/worktrees/emails-list/src/commands/segments/list.ts +0 -58
- package/.claude/worktrees/emails-list/src/commands/segments/utils.ts +0 -7
- package/.claude/worktrees/emails-list/src/commands/teams/index.ts +0 -10
- package/.claude/worktrees/emails-list/src/commands/teams/list.ts +0 -35
- package/.claude/worktrees/emails-list/src/commands/teams/remove.ts +0 -83
- package/.claude/worktrees/emails-list/src/commands/teams/switch.ts +0 -73
- package/.claude/worktrees/emails-list/src/commands/topics/create.ts +0 -73
- package/.claude/worktrees/emails-list/src/commands/topics/delete.ts +0 -47
- package/.claude/worktrees/emails-list/src/commands/topics/get.ts +0 -42
- package/.claude/worktrees/emails-list/src/commands/topics/index.ts +0 -42
- package/.claude/worktrees/emails-list/src/commands/topics/list.ts +0 -34
- package/.claude/worktrees/emails-list/src/commands/topics/update.ts +0 -59
- package/.claude/worktrees/emails-list/src/commands/topics/utils.ts +0 -16
- package/.claude/worktrees/emails-list/src/commands/webhooks/create.ts +0 -128
- package/.claude/worktrees/emails-list/src/commands/webhooks/delete.ts +0 -49
- package/.claude/worktrees/emails-list/src/commands/webhooks/get.ts +0 -42
- package/.claude/worktrees/emails-list/src/commands/webhooks/index.ts +0 -44
- package/.claude/worktrees/emails-list/src/commands/webhooks/list.ts +0 -55
- package/.claude/worktrees/emails-list/src/commands/webhooks/update.ts +0 -83
- package/.claude/worktrees/emails-list/src/commands/webhooks/utils.ts +0 -36
- package/.claude/worktrees/emails-list/src/commands/whoami.ts +0 -71
- package/.claude/worktrees/emails-list/src/lib/actions.ts +0 -157
- package/.claude/worktrees/emails-list/src/lib/client.ts +0 -34
- package/.claude/worktrees/emails-list/src/lib/config.ts +0 -211
- package/.claude/worktrees/emails-list/src/lib/files.ts +0 -15
- package/.claude/worktrees/emails-list/src/lib/help-text.ts +0 -38
- package/.claude/worktrees/emails-list/src/lib/output.ts +0 -54
- package/.claude/worktrees/emails-list/src/lib/pagination.ts +0 -36
- package/.claude/worktrees/emails-list/src/lib/prompts.ts +0 -149
- package/.claude/worktrees/emails-list/src/lib/spinner.ts +0 -93
- package/.claude/worktrees/emails-list/src/lib/table.ts +0 -57
- package/.claude/worktrees/emails-list/src/lib/tty.ts +0 -28
- package/.claude/worktrees/emails-list/src/lib/version.ts +0 -4
- package/.claude/worktrees/emails-list/tests/commands/api-keys/create.test.ts +0 -195
- package/.claude/worktrees/emails-list/tests/commands/api-keys/delete.test.ts +0 -156
- package/.claude/worktrees/emails-list/tests/commands/api-keys/list.test.ts +0 -133
- package/.claude/worktrees/emails-list/tests/commands/auth/login.test.ts +0 -119
- package/.claude/worktrees/emails-list/tests/commands/auth/logout.test.ts +0 -146
- package/.claude/worktrees/emails-list/tests/commands/broadcasts/create.test.ts +0 -447
- package/.claude/worktrees/emails-list/tests/commands/broadcasts/delete.test.ts +0 -182
- package/.claude/worktrees/emails-list/tests/commands/broadcasts/get.test.ts +0 -146
- package/.claude/worktrees/emails-list/tests/commands/broadcasts/list.test.ts +0 -196
- package/.claude/worktrees/emails-list/tests/commands/broadcasts/send.test.ts +0 -161
- package/.claude/worktrees/emails-list/tests/commands/broadcasts/update.test.ts +0 -283
- package/.claude/worktrees/emails-list/tests/commands/contact-properties/create.test.ts +0 -250
- package/.claude/worktrees/emails-list/tests/commands/contact-properties/delete.test.ts +0 -183
- package/.claude/worktrees/emails-list/tests/commands/contact-properties/get.test.ts +0 -144
- package/.claude/worktrees/emails-list/tests/commands/contact-properties/list.test.ts +0 -180
- package/.claude/worktrees/emails-list/tests/commands/contact-properties/update.test.ts +0 -216
- package/.claude/worktrees/emails-list/tests/commands/contacts/add-segment.test.ts +0 -188
- package/.claude/worktrees/emails-list/tests/commands/contacts/create.test.ts +0 -270
- package/.claude/worktrees/emails-list/tests/commands/contacts/delete.test.ts +0 -192
- package/.claude/worktrees/emails-list/tests/commands/contacts/get.test.ts +0 -148
- package/.claude/worktrees/emails-list/tests/commands/contacts/list.test.ts +0 -175
- package/.claude/worktrees/emails-list/tests/commands/contacts/remove-segment.test.ts +0 -166
- package/.claude/worktrees/emails-list/tests/commands/contacts/segments.test.ts +0 -167
- package/.claude/worktrees/emails-list/tests/commands/contacts/topics.test.ts +0 -163
- package/.claude/worktrees/emails-list/tests/commands/contacts/update-topics.test.ts +0 -247
- package/.claude/worktrees/emails-list/tests/commands/contacts/update.test.ts +0 -205
- package/.claude/worktrees/emails-list/tests/commands/doctor.test.ts +0 -165
- package/.claude/worktrees/emails-list/tests/commands/domains/create.test.ts +0 -192
- package/.claude/worktrees/emails-list/tests/commands/domains/delete.test.ts +0 -156
- package/.claude/worktrees/emails-list/tests/commands/domains/get.test.ts +0 -137
- package/.claude/worktrees/emails-list/tests/commands/domains/list.test.ts +0 -164
- package/.claude/worktrees/emails-list/tests/commands/domains/update.test.ts +0 -223
- package/.claude/worktrees/emails-list/tests/commands/domains/verify.test.ts +0 -117
- package/.claude/worktrees/emails-list/tests/commands/emails/batch.test.ts +0 -313
- package/.claude/worktrees/emails-list/tests/commands/emails/list.test.ts +0 -196
- package/.claude/worktrees/emails-list/tests/commands/emails/receiving/attachment.test.ts +0 -140
- package/.claude/worktrees/emails-list/tests/commands/emails/receiving/attachments.test.ts +0 -168
- package/.claude/worktrees/emails-list/tests/commands/emails/receiving/get.test.ts +0 -140
- package/.claude/worktrees/emails-list/tests/commands/emails/receiving/list.test.ts +0 -181
- package/.claude/worktrees/emails-list/tests/commands/emails/send.test.ts +0 -309
- package/.claude/worktrees/emails-list/tests/commands/segments/create.test.ts +0 -163
- package/.claude/worktrees/emails-list/tests/commands/segments/delete.test.ts +0 -182
- package/.claude/worktrees/emails-list/tests/commands/segments/get.test.ts +0 -137
- package/.claude/worktrees/emails-list/tests/commands/segments/list.test.ts +0 -173
- package/.claude/worktrees/emails-list/tests/commands/teams/list.test.ts +0 -63
- package/.claude/worktrees/emails-list/tests/commands/teams/remove.test.ts +0 -103
- package/.claude/worktrees/emails-list/tests/commands/teams/switch.test.ts +0 -96
- package/.claude/worktrees/emails-list/tests/commands/topics/create.test.ts +0 -191
- package/.claude/worktrees/emails-list/tests/commands/topics/delete.test.ts +0 -156
- package/.claude/worktrees/emails-list/tests/commands/topics/get.test.ts +0 -125
- package/.claude/worktrees/emails-list/tests/commands/topics/list.test.ts +0 -124
- package/.claude/worktrees/emails-list/tests/commands/topics/update.test.ts +0 -177
- package/.claude/worktrees/emails-list/tests/commands/webhooks/create.test.ts +0 -224
- package/.claude/worktrees/emails-list/tests/commands/webhooks/delete.test.ts +0 -156
- package/.claude/worktrees/emails-list/tests/commands/webhooks/get.test.ts +0 -125
- package/.claude/worktrees/emails-list/tests/commands/webhooks/list.test.ts +0 -177
- package/.claude/worktrees/emails-list/tests/commands/webhooks/update.test.ts +0 -206
- package/.claude/worktrees/emails-list/tests/commands/whoami.test.ts +0 -99
- package/.claude/worktrees/emails-list/tests/helpers.ts +0 -93
- package/.claude/worktrees/emails-list/tests/lib/client.test.ts +0 -71
- package/.claude/worktrees/emails-list/tests/lib/config.test.ts +0 -414
- package/.claude/worktrees/emails-list/tests/lib/files.test.ts +0 -65
- package/.claude/worktrees/emails-list/tests/lib/help-text.test.ts +0 -97
- package/.claude/worktrees/emails-list/tests/lib/output.test.ts +0 -127
- package/.claude/worktrees/emails-list/tests/lib/prompts.test.ts +0 -178
- package/.claude/worktrees/emails-list/tests/lib/spinner.test.ts +0 -146
- package/.claude/worktrees/emails-list/tests/lib/table.test.ts +0 -63
- package/.claude/worktrees/emails-list/tests/lib/tty.test.ts +0 -85
- package/.claude/worktrees/emails-list/tsconfig.json +0 -14
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { getConfigDir } from './config';
|
|
4
|
+
import { VERSION } from './version';
|
|
5
|
+
|
|
6
|
+
const CHECK_INTERVAL_MS = 1 * 60 * 60 * 1000; // 1 hour
|
|
7
|
+
export const GITHUB_RELEASES_URL =
|
|
8
|
+
'https://api.github.com/repos/resend/resend-cli/releases/latest';
|
|
9
|
+
|
|
10
|
+
type UpdateState = {
|
|
11
|
+
lastChecked: number;
|
|
12
|
+
latestVersion: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function getStatePath(): string {
|
|
16
|
+
return join(getConfigDir(), 'update-state.json');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function readState(): UpdateState | null {
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(readFileSync(getStatePath(), 'utf-8')) as UpdateState;
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function writeState(state: UpdateState): void {
|
|
28
|
+
const dir = getConfigDir();
|
|
29
|
+
if (!existsSync(dir)) {
|
|
30
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
31
|
+
}
|
|
32
|
+
writeFileSync(getStatePath(), JSON.stringify(state), { mode: 0o600 });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Compare two semver strings. Returns true if remote > local.
|
|
37
|
+
*/
|
|
38
|
+
function isNewer(local: string, remote: string): boolean {
|
|
39
|
+
const parse = (v: string) => v.replace(/^v/, '').split('.').map(Number);
|
|
40
|
+
const [lMaj, lMin, lPat] = parse(local);
|
|
41
|
+
const [rMaj, rMin, rPat] = parse(remote);
|
|
42
|
+
if (rMaj !== lMaj) {
|
|
43
|
+
return rMaj > lMaj;
|
|
44
|
+
}
|
|
45
|
+
if (rMin !== lMin) {
|
|
46
|
+
return rMin > lMin;
|
|
47
|
+
}
|
|
48
|
+
return rPat > lPat;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function fetchLatestVersion(): Promise<string | null> {
|
|
52
|
+
try {
|
|
53
|
+
const res = await fetch(GITHUB_RELEASES_URL, {
|
|
54
|
+
headers: { Accept: 'application/vnd.github.v3+json' },
|
|
55
|
+
signal: AbortSignal.timeout(5000),
|
|
56
|
+
});
|
|
57
|
+
if (!res.ok) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const data = (await res.json()) as {
|
|
61
|
+
tag_name?: string;
|
|
62
|
+
prerelease?: boolean;
|
|
63
|
+
draft?: boolean;
|
|
64
|
+
};
|
|
65
|
+
// /releases/latest already excludes prereleases, but guard anyway
|
|
66
|
+
if (data.prerelease || data.draft) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
const version = data.tag_name?.replace(/^v/, '');
|
|
70
|
+
if (!version || !/^\d+\.\d+\.\d+$/.test(version)) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
return version;
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function shouldSkipCheck(): boolean {
|
|
80
|
+
if (process.env.RESEND_NO_UPDATE_NOTIFIER === '1') {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
if (process.env.CI === 'true' || process.env.CI === '1') {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
if (process.env.GITHUB_ACTIONS) {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
if (!process.stdout.isTTY) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function detectInstallMethod(): string {
|
|
96
|
+
const execPath = process.execPath || process.argv[0] || '';
|
|
97
|
+
|
|
98
|
+
// Homebrew
|
|
99
|
+
if (/\/(Cellar|homebrew)\//i.test(execPath)) {
|
|
100
|
+
return 'brew upgrade resend';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// npm / npx global install
|
|
104
|
+
if (/node_modules/.test(execPath) || process.env.npm_execpath) {
|
|
105
|
+
return 'npm install -g resend-cli';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Install script (default install location)
|
|
109
|
+
if (/[/\\]\.resend[/\\]bin[/\\]/.test(execPath)) {
|
|
110
|
+
if (process.platform === 'win32') {
|
|
111
|
+
return 'irm https://resend.com/install.ps1 | iex';
|
|
112
|
+
}
|
|
113
|
+
return 'curl -fsSL https://resend.com/install.sh | bash';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Default: install script
|
|
117
|
+
if (process.platform === 'win32') {
|
|
118
|
+
return 'irm https://resend.com/install.ps1 | iex';
|
|
119
|
+
}
|
|
120
|
+
return 'curl -fsSL https://resend.com/install.sh | bash';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function formatNotice(latestVersion: string): string {
|
|
124
|
+
const upgrade = detectInstallMethod();
|
|
125
|
+
const isUrl = upgrade.startsWith('http');
|
|
126
|
+
|
|
127
|
+
const dim = '\x1B[2m';
|
|
128
|
+
const yellow = '\x1B[33m';
|
|
129
|
+
const cyan = '\x1B[36m';
|
|
130
|
+
const reset = '\x1B[0m';
|
|
131
|
+
|
|
132
|
+
return [
|
|
133
|
+
'',
|
|
134
|
+
`${dim}Update available: ${yellow}v${VERSION}${reset}${dim} → ${cyan}v${latestVersion}${reset}`,
|
|
135
|
+
`${dim}${isUrl ? 'Visit' : 'Run'}: ${cyan}${upgrade}${reset}`,
|
|
136
|
+
'',
|
|
137
|
+
].join('\n');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Check for updates and print a notice to stderr if one is available.
|
|
142
|
+
* Designed to be called after the main command completes — never blocks
|
|
143
|
+
* or throws.
|
|
144
|
+
*/
|
|
145
|
+
export async function checkForUpdates(): Promise<void> {
|
|
146
|
+
if (shouldSkipCheck()) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const state = readState();
|
|
151
|
+
const now = Date.now();
|
|
152
|
+
|
|
153
|
+
// If we have a cached check that's still fresh, just use it
|
|
154
|
+
if (state && now - state.lastChecked < CHECK_INTERVAL_MS) {
|
|
155
|
+
if (isNewer(VERSION, state.latestVersion)) {
|
|
156
|
+
process.stderr.write(formatNotice(state.latestVersion));
|
|
157
|
+
}
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Stale or missing — fetch in the background
|
|
162
|
+
const latest = await fetchLatestVersion();
|
|
163
|
+
if (!latest) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
writeState({ lastChecked: now, latestVersion: latest });
|
|
168
|
+
|
|
169
|
+
if (isNewer(VERSION, latest)) {
|
|
170
|
+
process.stderr.write(formatNotice(latest));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -116,4 +116,41 @@ describe('login command', () => {
|
|
|
116
116
|
// Original team should still exist
|
|
117
117
|
expect(data.teams.production.api_key).toBe('re_old_key_1234');
|
|
118
118
|
});
|
|
119
|
+
|
|
120
|
+
test('auto-switches to team specified via --team flag', async () => {
|
|
121
|
+
spies = setupOutputSpies();
|
|
122
|
+
|
|
123
|
+
const { Command } = await import('@commander-js/extra-typings');
|
|
124
|
+
const { loginCommand } = await import('../../../src/commands/auth/login');
|
|
125
|
+
const program = new Command()
|
|
126
|
+
.option('--team <name>')
|
|
127
|
+
.option('--json')
|
|
128
|
+
.option('--api-key <key>')
|
|
129
|
+
.option('-q, --quiet')
|
|
130
|
+
.addCommand(loginCommand);
|
|
131
|
+
|
|
132
|
+
// First store a default key
|
|
133
|
+
const configDir = join(tmpDir, 'resend');
|
|
134
|
+
mkdirSync(configDir, { recursive: true });
|
|
135
|
+
writeFileSync(
|
|
136
|
+
join(configDir, 'credentials.json'),
|
|
137
|
+
JSON.stringify({
|
|
138
|
+
active_team: 'default',
|
|
139
|
+
teams: { default: { api_key: 're_old_key_1234' } },
|
|
140
|
+
}),
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
await program.parseAsync(
|
|
144
|
+
['login', '--key', 're_staging_key_123', '--team', 'staging'],
|
|
145
|
+
{ from: 'user' },
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
// @ts-expect-error — reset parent to avoid polluting the shared singleton
|
|
149
|
+
loginCommand.parent = null;
|
|
150
|
+
|
|
151
|
+
const configPath = join(tmpDir, 'resend', 'credentials.json');
|
|
152
|
+
const data = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
153
|
+
expect(data.active_team).toBe('staging');
|
|
154
|
+
expect(data.teams.staging.api_key).toBe('re_staging_key_123');
|
|
155
|
+
});
|
|
119
156
|
});
|
package/tests/lib/config.test.ts
CHANGED
|
@@ -5,11 +5,12 @@ import { join } from 'node:path';
|
|
|
5
5
|
import {
|
|
6
6
|
getConfigDir,
|
|
7
7
|
listTeams,
|
|
8
|
-
|
|
8
|
+
removeApiKey,
|
|
9
9
|
resolveApiKey,
|
|
10
10
|
resolveTeamName,
|
|
11
11
|
setActiveTeam,
|
|
12
12
|
storeApiKey,
|
|
13
|
+
validateTeamName,
|
|
13
14
|
} from '../../src/lib/config';
|
|
14
15
|
import { captureTestEnv } from '../helpers';
|
|
15
16
|
|
|
@@ -354,7 +355,7 @@ describe('setActiveTeam', () => {
|
|
|
354
355
|
});
|
|
355
356
|
});
|
|
356
357
|
|
|
357
|
-
describe('
|
|
358
|
+
describe('removeApiKey', () => {
|
|
358
359
|
const restoreEnv = captureTestEnv();
|
|
359
360
|
let tmpDir: string;
|
|
360
361
|
|
|
@@ -375,7 +376,7 @@ describe('removeTeam', () => {
|
|
|
375
376
|
storeApiKey('re_default', 'default');
|
|
376
377
|
storeApiKey('re_staging', 'staging');
|
|
377
378
|
|
|
378
|
-
|
|
379
|
+
removeApiKey('staging');
|
|
379
380
|
|
|
380
381
|
const teams = listTeams();
|
|
381
382
|
expect(teams).toEqual([{ name: 'default', active: true }]);
|
|
@@ -386,7 +387,7 @@ describe('removeTeam', () => {
|
|
|
386
387
|
storeApiKey('re_staging', 'staging');
|
|
387
388
|
setActiveTeam('staging');
|
|
388
389
|
|
|
389
|
-
|
|
390
|
+
removeApiKey('staging');
|
|
390
391
|
|
|
391
392
|
const configPath = join(tmpDir, 'resend', 'credentials.json');
|
|
392
393
|
const data = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
@@ -396,7 +397,7 @@ describe('removeTeam', () => {
|
|
|
396
397
|
test('deletes file when last team removed', () => {
|
|
397
398
|
storeApiKey('re_only', 'only');
|
|
398
399
|
|
|
399
|
-
|
|
400
|
+
removeApiKey('only');
|
|
400
401
|
|
|
401
402
|
const { existsSync } = require('node:fs');
|
|
402
403
|
const configPath = join(tmpDir, 'resend', 'credentials.json');
|
|
@@ -405,10 +406,40 @@ describe('removeTeam', () => {
|
|
|
405
406
|
|
|
406
407
|
test('throws when team does not exist', () => {
|
|
407
408
|
storeApiKey('re_default');
|
|
408
|
-
expect(() =>
|
|
409
|
+
expect(() => removeApiKey('nonexistent')).toThrow('not found');
|
|
409
410
|
});
|
|
410
411
|
|
|
411
412
|
test('throws when no credentials file', () => {
|
|
412
|
-
expect(() =>
|
|
413
|
+
expect(() => removeApiKey('any')).toThrow('No credentials file');
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
describe('validateTeamName', () => {
|
|
418
|
+
test('accepts valid names', () => {
|
|
419
|
+
expect(validateTeamName('default')).toBeUndefined();
|
|
420
|
+
expect(validateTeamName('my-team')).toBeUndefined();
|
|
421
|
+
expect(validateTeamName('team_1')).toBeUndefined();
|
|
422
|
+
expect(validateTeamName('prod-2024')).toBeUndefined();
|
|
423
|
+
expect(validateTeamName('Production')).toBeUndefined();
|
|
424
|
+
expect(validateTeamName('MyTeam')).toBeUndefined();
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
test('rejects spaces and special characters', () => {
|
|
428
|
+
expect(validateTeamName('my team')).toContain('letters');
|
|
429
|
+
expect(validateTeamName('team@org')).toContain('letters');
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
test('rejects empty name', () => {
|
|
433
|
+
expect(validateTeamName('')).toContain('empty');
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
test('rejects names longer than 64 characters', () => {
|
|
437
|
+
const longName = 'a'.repeat(65);
|
|
438
|
+
expect(validateTeamName(longName)).toContain('64');
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
test('accepts name exactly 64 characters', () => {
|
|
442
|
+
const maxName = 'a'.repeat(64);
|
|
443
|
+
expect(validateTeamName(maxName)).toBeUndefined();
|
|
413
444
|
});
|
|
414
445
|
});
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, spyOn, test } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
rmSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from 'node:fs';
|
|
9
|
+
import { tmpdir } from 'node:os';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { checkForUpdates } from '../../src/lib/update-check';
|
|
12
|
+
import { VERSION } from '../../src/lib/version';
|
|
13
|
+
import { captureTestEnv } from '../helpers';
|
|
14
|
+
|
|
15
|
+
// Use a version guaranteed to be "newer" than whatever VERSION is
|
|
16
|
+
const NEWER_VERSION = '99.0.0';
|
|
17
|
+
|
|
18
|
+
const testConfigDir = join(tmpdir(), `resend-update-check-test-${process.pid}`);
|
|
19
|
+
const testResendDir = join(testConfigDir, 'resend');
|
|
20
|
+
const statePath = join(testResendDir, 'update-state.json');
|
|
21
|
+
|
|
22
|
+
describe('checkForUpdates', () => {
|
|
23
|
+
const restoreEnv = captureTestEnv();
|
|
24
|
+
let stderrOutput: string;
|
|
25
|
+
let stderrSpy: ReturnType<typeof spyOn>;
|
|
26
|
+
let fetchSpy: ReturnType<typeof spyOn>;
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
mkdirSync(testResendDir, { recursive: true });
|
|
30
|
+
stderrOutput = '';
|
|
31
|
+
stderrSpy = spyOn(process.stderr, 'write').mockImplementation((chunk) => {
|
|
32
|
+
stderrOutput += String(chunk);
|
|
33
|
+
return true;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Point getConfigDir() at our temp dir via XDG_CONFIG_HOME
|
|
37
|
+
process.env.XDG_CONFIG_HOME = testConfigDir;
|
|
38
|
+
|
|
39
|
+
// Ensure TTY and no CI
|
|
40
|
+
Object.defineProperty(process.stdout, 'isTTY', {
|
|
41
|
+
value: true,
|
|
42
|
+
writable: true,
|
|
43
|
+
});
|
|
44
|
+
delete process.env.CI;
|
|
45
|
+
delete process.env.GITHUB_ACTIONS;
|
|
46
|
+
delete process.env.RESEND_NO_UPDATE_NOTIFIER;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
stderrSpy.mockRestore();
|
|
51
|
+
if (fetchSpy) {
|
|
52
|
+
fetchSpy.mockRestore();
|
|
53
|
+
}
|
|
54
|
+
restoreEnv();
|
|
55
|
+
if (existsSync(testConfigDir)) {
|
|
56
|
+
rmSync(testConfigDir, { recursive: true, force: true });
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
function mockFetch(tagName: string, extra: Record<string, unknown> = {}) {
|
|
61
|
+
fetchSpy = spyOn(globalThis, 'fetch').mockResolvedValue({
|
|
62
|
+
ok: true,
|
|
63
|
+
json: async () => ({ tag_name: tagName, ...extra }),
|
|
64
|
+
} as Response);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function mockFetchFailure() {
|
|
68
|
+
fetchSpy = spyOn(globalThis, 'fetch').mockRejectedValue(
|
|
69
|
+
new Error('network error'),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
test('skips check when RESEND_NO_UPDATE_NOTIFIER=1', async () => {
|
|
74
|
+
process.env.RESEND_NO_UPDATE_NOTIFIER = '1';
|
|
75
|
+
await checkForUpdates();
|
|
76
|
+
expect(stderrOutput).toBe('');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('skips check when CI=true', async () => {
|
|
80
|
+
process.env.CI = 'true';
|
|
81
|
+
await checkForUpdates();
|
|
82
|
+
expect(stderrOutput).toBe('');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('skips check when not a TTY', async () => {
|
|
86
|
+
Object.defineProperty(process.stdout, 'isTTY', {
|
|
87
|
+
value: undefined,
|
|
88
|
+
writable: true,
|
|
89
|
+
});
|
|
90
|
+
await checkForUpdates();
|
|
91
|
+
expect(stderrOutput).toBe('');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('prints notice when newer version available from fresh fetch', async () => {
|
|
95
|
+
mockFetch(`v${NEWER_VERSION}`);
|
|
96
|
+
await checkForUpdates();
|
|
97
|
+
|
|
98
|
+
expect(stderrOutput).toContain('Update available');
|
|
99
|
+
expect(stderrOutput).toContain(`v${VERSION}`);
|
|
100
|
+
expect(stderrOutput).toContain(`v${NEWER_VERSION}`);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('prints nothing when already on latest', async () => {
|
|
104
|
+
mockFetch(`v${VERSION}`);
|
|
105
|
+
await checkForUpdates();
|
|
106
|
+
expect(stderrOutput).toBe('');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('uses cached state when fresh (no fetch)', async () => {
|
|
110
|
+
writeFileSync(
|
|
111
|
+
statePath,
|
|
112
|
+
JSON.stringify({ lastChecked: Date.now(), latestVersion: NEWER_VERSION }),
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
mockFetch(`v${NEWER_VERSION}`);
|
|
116
|
+
await checkForUpdates();
|
|
117
|
+
|
|
118
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
119
|
+
expect(stderrOutput).toContain(`v${NEWER_VERSION}`);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('refetches when cache is stale', async () => {
|
|
123
|
+
const staleTime = Date.now() - 25 * 60 * 60 * 1000; // 25 hours ago
|
|
124
|
+
writeFileSync(
|
|
125
|
+
statePath,
|
|
126
|
+
JSON.stringify({ lastChecked: staleTime, latestVersion: VERSION }),
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
mockFetch(`v${NEWER_VERSION}`);
|
|
130
|
+
await checkForUpdates();
|
|
131
|
+
|
|
132
|
+
expect(fetchSpy).toHaveBeenCalled();
|
|
133
|
+
expect(stderrOutput).toContain(`v${NEWER_VERSION}`);
|
|
134
|
+
// Verify cache was updated
|
|
135
|
+
const state = JSON.parse(readFileSync(statePath, 'utf-8'));
|
|
136
|
+
expect(state.latestVersion).toBe(NEWER_VERSION);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('handles fetch failure gracefully', async () => {
|
|
140
|
+
mockFetchFailure();
|
|
141
|
+
await checkForUpdates();
|
|
142
|
+
expect(stderrOutput).toBe('');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('writes state file after successful fetch', async () => {
|
|
146
|
+
mockFetch(`v${VERSION}`);
|
|
147
|
+
await checkForUpdates();
|
|
148
|
+
|
|
149
|
+
expect(existsSync(statePath)).toBe(true);
|
|
150
|
+
const state = JSON.parse(readFileSync(statePath, 'utf-8'));
|
|
151
|
+
expect(state.latestVersion).toBe(VERSION);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('ignores prerelease versions', async () => {
|
|
155
|
+
fetchSpy = spyOn(globalThis, 'fetch').mockResolvedValue({
|
|
156
|
+
ok: true,
|
|
157
|
+
json: async () => ({ tag_name: 'v99.0.0', prerelease: true }),
|
|
158
|
+
} as Response);
|
|
159
|
+
|
|
160
|
+
await checkForUpdates();
|
|
161
|
+
expect(stderrOutput).toBe('');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('ignores non-semver tag names', async () => {
|
|
165
|
+
mockFetch('canary-20260311');
|
|
166
|
+
await checkForUpdates();
|
|
167
|
+
expect(stderrOutput).toBe('');
|
|
168
|
+
});
|
|
169
|
+
});
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
|
|
3
|
-
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
4
|
-
if (!eventPath) {
|
|
5
|
-
console.error(
|
|
6
|
-
'GITHUB_EVENT_PATH is not set. This script must run inside GitHub Actions.',
|
|
7
|
-
);
|
|
8
|
-
process.exit(1);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const eventJson = JSON.parse(fs.readFileSync(eventPath, 'utf8'));
|
|
12
|
-
const prTitle = eventJson.pull_request?.title;
|
|
13
|
-
if (!prTitle) {
|
|
14
|
-
console.error('Could not read pull_request.title from event payload.');
|
|
15
|
-
process.exit(1);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const isValidType = (title) =>
|
|
19
|
-
/^(feat|fix|chore|refactor)(\([a-zA-Z0-9-]+\))?:\s[a-z0-9].*$/.test(title);
|
|
20
|
-
|
|
21
|
-
const validateTitle = (title) => {
|
|
22
|
-
if (!isValidType(title)) {
|
|
23
|
-
console.error(
|
|
24
|
-
`PR title does not follow the required format "[type]: [description]"
|
|
25
|
-
Examples: "feat: add --cc flag" | "fix: domain fetch timeout"
|
|
26
|
-
Types: feat · fix · chore · refactor
|
|
27
|
-
Rules: lowercase after the colon`,
|
|
28
|
-
);
|
|
29
|
-
process.exit(1);
|
|
30
|
-
}
|
|
31
|
-
console.info(`PR title valid: "${title}"`);
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
validateTitle(prTitle);
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
on:
|
|
3
|
-
push:
|
|
4
|
-
branches: [main]
|
|
5
|
-
pull_request:
|
|
6
|
-
concurrency:
|
|
7
|
-
group: ci-${{ github.ref }}
|
|
8
|
-
cancel-in-progress: true
|
|
9
|
-
jobs:
|
|
10
|
-
lint:
|
|
11
|
-
runs-on: blacksmith-2vcpu-ubuntu-2204
|
|
12
|
-
steps:
|
|
13
|
-
- uses: actions/checkout@v4
|
|
14
|
-
- uses: oven-sh/setup-bun@v2
|
|
15
|
-
- run: bun install --frozen-lockfile
|
|
16
|
-
- run: bun run lint
|
|
17
|
-
|
|
18
|
-
typecheck:
|
|
19
|
-
runs-on: blacksmith-2vcpu-ubuntu-2204
|
|
20
|
-
steps:
|
|
21
|
-
- uses: actions/checkout@v4
|
|
22
|
-
- uses: oven-sh/setup-bun@v2
|
|
23
|
-
- run: bun install --frozen-lockfile
|
|
24
|
-
- run: bun run typecheck
|
|
25
|
-
|
|
26
|
-
test:
|
|
27
|
-
runs-on: blacksmith-2vcpu-ubuntu-2204
|
|
28
|
-
steps:
|
|
29
|
-
- uses: actions/checkout@v4
|
|
30
|
-
- uses: oven-sh/setup-bun@v2
|
|
31
|
-
- run: bun install --frozen-lockfile
|
|
32
|
-
- run: bun run test
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
name: PR Title Check
|
|
2
|
-
on:
|
|
3
|
-
pull_request:
|
|
4
|
-
types: [opened, edited, synchronize, reopened]
|
|
5
|
-
permissions:
|
|
6
|
-
contents: read
|
|
7
|
-
pull-requests: read
|
|
8
|
-
jobs:
|
|
9
|
-
pr-title-check:
|
|
10
|
-
runs-on: ubuntu-latest
|
|
11
|
-
steps:
|
|
12
|
-
- uses: actions/checkout@v4
|
|
13
|
-
- run: node .github/scripts/pr-title-check.js
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
name: Release
|
|
2
|
-
on:
|
|
3
|
-
push:
|
|
4
|
-
tags:
|
|
5
|
-
- 'v*'
|
|
6
|
-
permissions:
|
|
7
|
-
contents: write
|
|
8
|
-
concurrency:
|
|
9
|
-
group: release
|
|
10
|
-
cancel-in-progress: false
|
|
11
|
-
jobs:
|
|
12
|
-
release:
|
|
13
|
-
runs-on: blacksmith-2vcpu-ubuntu-2204
|
|
14
|
-
steps:
|
|
15
|
-
- uses: actions/checkout@v4
|
|
16
|
-
|
|
17
|
-
- uses: oven-sh/setup-bun@v2
|
|
18
|
-
|
|
19
|
-
- name: Install dependencies
|
|
20
|
-
run: bun install --frozen-lockfile
|
|
21
|
-
|
|
22
|
-
- name: Build binaries
|
|
23
|
-
run: |
|
|
24
|
-
bun build --compile --minify --sourcemap --bytecode src/cli.ts --target=bun-darwin-arm64 --outfile dist/resend-darwin-arm64
|
|
25
|
-
bun build --compile --minify --sourcemap --bytecode src/cli.ts --target=bun-darwin-x64 --outfile dist/resend-darwin-x64
|
|
26
|
-
bun build --compile --minify --sourcemap --bytecode src/cli.ts --target=bun-linux-x64 --outfile dist/resend-linux-x64
|
|
27
|
-
bun build --compile --minify --sourcemap --bytecode src/cli.ts --target=bun-linux-arm64 --outfile dist/resend-linux-arm64
|
|
28
|
-
bun build --compile --minify --sourcemap --bytecode src/cli.ts --target=bun-windows-x64 --outfile dist/resend-windows-x64.exe
|
|
29
|
-
|
|
30
|
-
- name: Verify binaries
|
|
31
|
-
run: |
|
|
32
|
-
for f in dist/resend-darwin-arm64 dist/resend-darwin-x64 \
|
|
33
|
-
dist/resend-linux-x64 dist/resend-linux-arm64 \
|
|
34
|
-
dist/resend-windows-x64.exe; do
|
|
35
|
-
[ -s "$f" ] || { echo "Missing or empty: $f"; exit 1; }
|
|
36
|
-
done
|
|
37
|
-
|
|
38
|
-
- name: Package archives
|
|
39
|
-
run: |
|
|
40
|
-
cd dist
|
|
41
|
-
for bin in resend-darwin-arm64 resend-darwin-x64 resend-linux-x64 resend-linux-arm64; do
|
|
42
|
-
cp "$bin" resend
|
|
43
|
-
chmod +x resend
|
|
44
|
-
tar -czf "${bin}.tar.gz" resend
|
|
45
|
-
rm resend
|
|
46
|
-
done
|
|
47
|
-
cp resend-windows-x64.exe resend.exe
|
|
48
|
-
zip resend-windows-x64.zip resend.exe
|
|
49
|
-
rm resend.exe
|
|
50
|
-
|
|
51
|
-
- name: Create GitHub Release
|
|
52
|
-
uses: softprops/action-gh-release@v2
|
|
53
|
-
with:
|
|
54
|
-
generate_release_notes: true
|
|
55
|
-
body: |
|
|
56
|
-
## Install
|
|
57
|
-
|
|
58
|
-
**macOS / Linux**
|
|
59
|
-
```sh
|
|
60
|
-
curl -fsSL https://resend.com/install.sh | bash
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
**Windows (PowerShell)**
|
|
64
|
-
```pwsh
|
|
65
|
-
irm https://resend.com/install.ps1 | iex
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
**GitHub CLI** _(use while repo is private)_
|
|
69
|
-
```sh
|
|
70
|
-
gh release download --repo resend/resend-cli --pattern "resend-darwin-arm64.tar.gz"
|
|
71
|
-
tar -xzf resend-darwin-arm64.tar.gz && sudo mv resend /usr/local/bin/ && rm resend-darwin-arm64.tar.gz
|
|
72
|
-
```
|
|
73
|
-
files: |
|
|
74
|
-
dist/resend-darwin-arm64.tar.gz
|
|
75
|
-
dist/resend-darwin-x64.tar.gz
|
|
76
|
-
dist/resend-linux-x64.tar.gz
|
|
77
|
-
dist/resend-linux-arm64.tar.gz
|
|
78
|
-
dist/resend-windows-x64.zip
|
|
79
|
-
|
|
80
|
-
notify-tap:
|
|
81
|
-
name: Trigger Homebrew Tap Update
|
|
82
|
-
needs: release
|
|
83
|
-
runs-on: ubuntu-latest
|
|
84
|
-
permissions:
|
|
85
|
-
contents: read
|
|
86
|
-
steps:
|
|
87
|
-
- name: Dispatch publish-release workflow on tap
|
|
88
|
-
env:
|
|
89
|
-
GH_TOKEN: ${{ secrets.RELEASE_CLI_ON_HOMEBREW }}
|
|
90
|
-
run: |
|
|
91
|
-
gh workflow run publish-release.yml \
|
|
92
|
-
--repo resend/homebrew-cli \
|
|
93
|
-
--field version="${GITHUB_REF_NAME}"
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## [0.2.0] - 2026-02-18
|
|
4
|
-
|
|
5
|
-
### Added
|
|
6
|
-
|
|
7
|
-
- `resend domains` — create, verify, get, list, update, delete sending domains
|
|
8
|
-
- `resend api-keys` — create, list, delete API keys
|
|
9
|
-
- `resend broadcasts` — full broadcast lifecycle (create, send, get, list, update, delete)
|
|
10
|
-
- `resend contacts` — manage contacts, segments, and topics across all CRUD operations
|
|
11
|
-
- `resend emails batch` — send up to 100 emails in a single request from a JSON file
|
|
12
|
-
- Shared pagination (`--limit`, `--after`, `--before`) on all list commands
|
|
13
|
-
- `--html-file` flag on `emails send` and `broadcasts create` to read body from a file
|
|
14
|
-
|
|
15
|
-
### Fixed
|
|
16
|
-
|
|
17
|
-
- `isInteractive()` now checks both `stdin` and `stdout` TTY — CI environments are correctly detected as non-interactive
|
|
18
|
-
- `domains delete` now returns a consistent `{ id, deleted: true }` object instead of an empty `{}`
|
|
19
|
-
|
|
20
|
-
### Changed
|
|
21
|
-
|
|
22
|
-
- All delete commands return a uniform `{ object, id, deleted: true }` response
|
|
23
|
-
- `--help` improved across all commands with output shape, error codes, and usage examples
|
|
24
|
-
|
|
25
|
-
---
|
|
26
|
-
|
|
27
|
-
## [0.1.0] - 2026-02-18
|
|
28
|
-
|
|
29
|
-
- Initial release: `auth login`, `emails send`, `doctor`
|
|
30
|
-
- Auto JSON output when stdout is not a TTY (`--json`)
|
|
31
|
-
- Cross-platform binaries for macOS, Linux, and Windows via GitHub Actions
|