resend-cli 1.2.0 → 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.
Files changed (193) hide show
  1. package/.github/workflows/release.yml +35 -8
  2. package/README.md +12 -1
  3. package/biome.json +1 -1
  4. package/bun.lock +0 -3
  5. package/package.json +3 -4
  6. package/src/cli.ts +23 -5
  7. package/src/commands/auth/login.ts +10 -8
  8. package/src/commands/doctor.ts +30 -112
  9. package/src/lib/client.ts +3 -0
  10. package/src/lib/config.ts +2 -3
  11. package/src/lib/spinner.ts +17 -10
  12. package/src/lib/update-check.ts +172 -0
  13. package/tests/commands/auth/login.test.ts +3 -1
  14. package/tests/lib/config.test.ts +4 -6
  15. package/tests/lib/update-check.test.ts +169 -0
  16. package/.claude/worktrees/emails-list/.claude/settings.local.json +0 -5
  17. package/.claude/worktrees/emails-list/.github/scripts/pr-title-check.js +0 -34
  18. package/.claude/worktrees/emails-list/.github/workflows/ci.yml +0 -32
  19. package/.claude/worktrees/emails-list/.github/workflows/pr-title-check.yml +0 -13
  20. package/.claude/worktrees/emails-list/.github/workflows/release.yml +0 -93
  21. package/.claude/worktrees/emails-list/CHANGELOG.md +0 -31
  22. package/.claude/worktrees/emails-list/LICENSE +0 -21
  23. package/.claude/worktrees/emails-list/README.md +0 -424
  24. package/.claude/worktrees/emails-list/biome.json +0 -36
  25. package/.claude/worktrees/emails-list/bun.lock +0 -76
  26. package/.claude/worktrees/emails-list/bunfig.toml +0 -2
  27. package/.claude/worktrees/emails-list/install.ps1 +0 -140
  28. package/.claude/worktrees/emails-list/install.sh +0 -301
  29. package/.claude/worktrees/emails-list/package.json +0 -43
  30. package/.claude/worktrees/emails-list/renovate.json +0 -6
  31. package/.claude/worktrees/emails-list/src/cli.ts +0 -74
  32. package/.claude/worktrees/emails-list/src/commands/api-keys/create.ts +0 -114
  33. package/.claude/worktrees/emails-list/src/commands/api-keys/delete.ts +0 -47
  34. package/.claude/worktrees/emails-list/src/commands/api-keys/index.ts +0 -26
  35. package/.claude/worktrees/emails-list/src/commands/api-keys/list.ts +0 -35
  36. package/.claude/worktrees/emails-list/src/commands/api-keys/utils.ts +0 -8
  37. package/.claude/worktrees/emails-list/src/commands/auth/index.ts +0 -20
  38. package/.claude/worktrees/emails-list/src/commands/auth/login.ts +0 -207
  39. package/.claude/worktrees/emails-list/src/commands/auth/logout.ts +0 -105
  40. package/.claude/worktrees/emails-list/src/commands/broadcasts/create.ts +0 -196
  41. package/.claude/worktrees/emails-list/src/commands/broadcasts/delete.ts +0 -46
  42. package/.claude/worktrees/emails-list/src/commands/broadcasts/get.ts +0 -59
  43. package/.claude/worktrees/emails-list/src/commands/broadcasts/index.ts +0 -43
  44. package/.claude/worktrees/emails-list/src/commands/broadcasts/list.ts +0 -60
  45. package/.claude/worktrees/emails-list/src/commands/broadcasts/send.ts +0 -56
  46. package/.claude/worktrees/emails-list/src/commands/broadcasts/update.ts +0 -95
  47. package/.claude/worktrees/emails-list/src/commands/broadcasts/utils.ts +0 -35
  48. package/.claude/worktrees/emails-list/src/commands/contact-properties/create.ts +0 -118
  49. package/.claude/worktrees/emails-list/src/commands/contact-properties/delete.ts +0 -48
  50. package/.claude/worktrees/emails-list/src/commands/contact-properties/get.ts +0 -46
  51. package/.claude/worktrees/emails-list/src/commands/contact-properties/index.ts +0 -48
  52. package/.claude/worktrees/emails-list/src/commands/contact-properties/list.ts +0 -68
  53. package/.claude/worktrees/emails-list/src/commands/contact-properties/update.ts +0 -88
  54. package/.claude/worktrees/emails-list/src/commands/contact-properties/utils.ts +0 -17
  55. package/.claude/worktrees/emails-list/src/commands/contacts/add-segment.ts +0 -78
  56. package/.claude/worktrees/emails-list/src/commands/contacts/create.ts +0 -122
  57. package/.claude/worktrees/emails-list/src/commands/contacts/delete.ts +0 -49
  58. package/.claude/worktrees/emails-list/src/commands/contacts/get.ts +0 -53
  59. package/.claude/worktrees/emails-list/src/commands/contacts/index.ts +0 -58
  60. package/.claude/worktrees/emails-list/src/commands/contacts/list.ts +0 -57
  61. package/.claude/worktrees/emails-list/src/commands/contacts/remove-segment.ts +0 -48
  62. package/.claude/worktrees/emails-list/src/commands/contacts/segments.ts +0 -39
  63. package/.claude/worktrees/emails-list/src/commands/contacts/topics.ts +0 -45
  64. package/.claude/worktrees/emails-list/src/commands/contacts/update-topics.ts +0 -90
  65. package/.claude/worktrees/emails-list/src/commands/contacts/update.ts +0 -77
  66. package/.claude/worktrees/emails-list/src/commands/contacts/utils.ts +0 -119
  67. package/.claude/worktrees/emails-list/src/commands/doctor.ts +0 -298
  68. package/.claude/worktrees/emails-list/src/commands/domains/create.ts +0 -83
  69. package/.claude/worktrees/emails-list/src/commands/domains/delete.ts +0 -42
  70. package/.claude/worktrees/emails-list/src/commands/domains/get.ts +0 -47
  71. package/.claude/worktrees/emails-list/src/commands/domains/index.ts +0 -35
  72. package/.claude/worktrees/emails-list/src/commands/domains/list.ts +0 -53
  73. package/.claude/worktrees/emails-list/src/commands/domains/update.ts +0 -75
  74. package/.claude/worktrees/emails-list/src/commands/domains/utils.ts +0 -44
  75. package/.claude/worktrees/emails-list/src/commands/domains/verify.ts +0 -38
  76. package/.claude/worktrees/emails-list/src/commands/emails/batch.ts +0 -140
  77. package/.claude/worktrees/emails-list/src/commands/emails/index.ts +0 -28
  78. package/.claude/worktrees/emails-list/src/commands/emails/list.ts +0 -73
  79. package/.claude/worktrees/emails-list/src/commands/emails/receiving/attachment.ts +0 -55
  80. package/.claude/worktrees/emails-list/src/commands/emails/receiving/attachments.ts +0 -68
  81. package/.claude/worktrees/emails-list/src/commands/emails/receiving/get.ts +0 -58
  82. package/.claude/worktrees/emails-list/src/commands/emails/receiving/index.ts +0 -28
  83. package/.claude/worktrees/emails-list/src/commands/emails/receiving/list.ts +0 -59
  84. package/.claude/worktrees/emails-list/src/commands/emails/receiving/utils.ts +0 -38
  85. package/.claude/worktrees/emails-list/src/commands/emails/send.ts +0 -189
  86. package/.claude/worktrees/emails-list/src/commands/open.ts +0 -24
  87. package/.claude/worktrees/emails-list/src/commands/segments/create.ts +0 -50
  88. package/.claude/worktrees/emails-list/src/commands/segments/delete.ts +0 -47
  89. package/.claude/worktrees/emails-list/src/commands/segments/get.ts +0 -38
  90. package/.claude/worktrees/emails-list/src/commands/segments/index.ts +0 -36
  91. package/.claude/worktrees/emails-list/src/commands/segments/list.ts +0 -58
  92. package/.claude/worktrees/emails-list/src/commands/segments/utils.ts +0 -7
  93. package/.claude/worktrees/emails-list/src/commands/teams/index.ts +0 -10
  94. package/.claude/worktrees/emails-list/src/commands/teams/list.ts +0 -35
  95. package/.claude/worktrees/emails-list/src/commands/teams/remove.ts +0 -83
  96. package/.claude/worktrees/emails-list/src/commands/teams/switch.ts +0 -73
  97. package/.claude/worktrees/emails-list/src/commands/topics/create.ts +0 -73
  98. package/.claude/worktrees/emails-list/src/commands/topics/delete.ts +0 -47
  99. package/.claude/worktrees/emails-list/src/commands/topics/get.ts +0 -42
  100. package/.claude/worktrees/emails-list/src/commands/topics/index.ts +0 -42
  101. package/.claude/worktrees/emails-list/src/commands/topics/list.ts +0 -34
  102. package/.claude/worktrees/emails-list/src/commands/topics/update.ts +0 -59
  103. package/.claude/worktrees/emails-list/src/commands/topics/utils.ts +0 -16
  104. package/.claude/worktrees/emails-list/src/commands/webhooks/create.ts +0 -128
  105. package/.claude/worktrees/emails-list/src/commands/webhooks/delete.ts +0 -49
  106. package/.claude/worktrees/emails-list/src/commands/webhooks/get.ts +0 -42
  107. package/.claude/worktrees/emails-list/src/commands/webhooks/index.ts +0 -44
  108. package/.claude/worktrees/emails-list/src/commands/webhooks/list.ts +0 -55
  109. package/.claude/worktrees/emails-list/src/commands/webhooks/update.ts +0 -83
  110. package/.claude/worktrees/emails-list/src/commands/webhooks/utils.ts +0 -36
  111. package/.claude/worktrees/emails-list/src/commands/whoami.ts +0 -71
  112. package/.claude/worktrees/emails-list/src/lib/actions.ts +0 -157
  113. package/.claude/worktrees/emails-list/src/lib/client.ts +0 -34
  114. package/.claude/worktrees/emails-list/src/lib/config.ts +0 -211
  115. package/.claude/worktrees/emails-list/src/lib/files.ts +0 -15
  116. package/.claude/worktrees/emails-list/src/lib/help-text.ts +0 -38
  117. package/.claude/worktrees/emails-list/src/lib/output.ts +0 -54
  118. package/.claude/worktrees/emails-list/src/lib/pagination.ts +0 -36
  119. package/.claude/worktrees/emails-list/src/lib/prompts.ts +0 -149
  120. package/.claude/worktrees/emails-list/src/lib/spinner.ts +0 -93
  121. package/.claude/worktrees/emails-list/src/lib/table.ts +0 -57
  122. package/.claude/worktrees/emails-list/src/lib/tty.ts +0 -28
  123. package/.claude/worktrees/emails-list/src/lib/version.ts +0 -4
  124. package/.claude/worktrees/emails-list/tests/commands/api-keys/create.test.ts +0 -195
  125. package/.claude/worktrees/emails-list/tests/commands/api-keys/delete.test.ts +0 -156
  126. package/.claude/worktrees/emails-list/tests/commands/api-keys/list.test.ts +0 -133
  127. package/.claude/worktrees/emails-list/tests/commands/auth/login.test.ts +0 -119
  128. package/.claude/worktrees/emails-list/tests/commands/auth/logout.test.ts +0 -146
  129. package/.claude/worktrees/emails-list/tests/commands/broadcasts/create.test.ts +0 -447
  130. package/.claude/worktrees/emails-list/tests/commands/broadcasts/delete.test.ts +0 -182
  131. package/.claude/worktrees/emails-list/tests/commands/broadcasts/get.test.ts +0 -146
  132. package/.claude/worktrees/emails-list/tests/commands/broadcasts/list.test.ts +0 -196
  133. package/.claude/worktrees/emails-list/tests/commands/broadcasts/send.test.ts +0 -161
  134. package/.claude/worktrees/emails-list/tests/commands/broadcasts/update.test.ts +0 -283
  135. package/.claude/worktrees/emails-list/tests/commands/contact-properties/create.test.ts +0 -250
  136. package/.claude/worktrees/emails-list/tests/commands/contact-properties/delete.test.ts +0 -183
  137. package/.claude/worktrees/emails-list/tests/commands/contact-properties/get.test.ts +0 -144
  138. package/.claude/worktrees/emails-list/tests/commands/contact-properties/list.test.ts +0 -180
  139. package/.claude/worktrees/emails-list/tests/commands/contact-properties/update.test.ts +0 -216
  140. package/.claude/worktrees/emails-list/tests/commands/contacts/add-segment.test.ts +0 -188
  141. package/.claude/worktrees/emails-list/tests/commands/contacts/create.test.ts +0 -270
  142. package/.claude/worktrees/emails-list/tests/commands/contacts/delete.test.ts +0 -192
  143. package/.claude/worktrees/emails-list/tests/commands/contacts/get.test.ts +0 -148
  144. package/.claude/worktrees/emails-list/tests/commands/contacts/list.test.ts +0 -175
  145. package/.claude/worktrees/emails-list/tests/commands/contacts/remove-segment.test.ts +0 -166
  146. package/.claude/worktrees/emails-list/tests/commands/contacts/segments.test.ts +0 -167
  147. package/.claude/worktrees/emails-list/tests/commands/contacts/topics.test.ts +0 -163
  148. package/.claude/worktrees/emails-list/tests/commands/contacts/update-topics.test.ts +0 -247
  149. package/.claude/worktrees/emails-list/tests/commands/contacts/update.test.ts +0 -205
  150. package/.claude/worktrees/emails-list/tests/commands/doctor.test.ts +0 -165
  151. package/.claude/worktrees/emails-list/tests/commands/domains/create.test.ts +0 -192
  152. package/.claude/worktrees/emails-list/tests/commands/domains/delete.test.ts +0 -156
  153. package/.claude/worktrees/emails-list/tests/commands/domains/get.test.ts +0 -137
  154. package/.claude/worktrees/emails-list/tests/commands/domains/list.test.ts +0 -164
  155. package/.claude/worktrees/emails-list/tests/commands/domains/update.test.ts +0 -223
  156. package/.claude/worktrees/emails-list/tests/commands/domains/verify.test.ts +0 -117
  157. package/.claude/worktrees/emails-list/tests/commands/emails/batch.test.ts +0 -313
  158. package/.claude/worktrees/emails-list/tests/commands/emails/list.test.ts +0 -196
  159. package/.claude/worktrees/emails-list/tests/commands/emails/receiving/attachment.test.ts +0 -140
  160. package/.claude/worktrees/emails-list/tests/commands/emails/receiving/attachments.test.ts +0 -168
  161. package/.claude/worktrees/emails-list/tests/commands/emails/receiving/get.test.ts +0 -140
  162. package/.claude/worktrees/emails-list/tests/commands/emails/receiving/list.test.ts +0 -181
  163. package/.claude/worktrees/emails-list/tests/commands/emails/send.test.ts +0 -309
  164. package/.claude/worktrees/emails-list/tests/commands/segments/create.test.ts +0 -163
  165. package/.claude/worktrees/emails-list/tests/commands/segments/delete.test.ts +0 -182
  166. package/.claude/worktrees/emails-list/tests/commands/segments/get.test.ts +0 -137
  167. package/.claude/worktrees/emails-list/tests/commands/segments/list.test.ts +0 -173
  168. package/.claude/worktrees/emails-list/tests/commands/teams/list.test.ts +0 -63
  169. package/.claude/worktrees/emails-list/tests/commands/teams/remove.test.ts +0 -103
  170. package/.claude/worktrees/emails-list/tests/commands/teams/switch.test.ts +0 -96
  171. package/.claude/worktrees/emails-list/tests/commands/topics/create.test.ts +0 -191
  172. package/.claude/worktrees/emails-list/tests/commands/topics/delete.test.ts +0 -156
  173. package/.claude/worktrees/emails-list/tests/commands/topics/get.test.ts +0 -125
  174. package/.claude/worktrees/emails-list/tests/commands/topics/list.test.ts +0 -124
  175. package/.claude/worktrees/emails-list/tests/commands/topics/update.test.ts +0 -177
  176. package/.claude/worktrees/emails-list/tests/commands/webhooks/create.test.ts +0 -224
  177. package/.claude/worktrees/emails-list/tests/commands/webhooks/delete.test.ts +0 -156
  178. package/.claude/worktrees/emails-list/tests/commands/webhooks/get.test.ts +0 -125
  179. package/.claude/worktrees/emails-list/tests/commands/webhooks/list.test.ts +0 -177
  180. package/.claude/worktrees/emails-list/tests/commands/webhooks/update.test.ts +0 -206
  181. package/.claude/worktrees/emails-list/tests/commands/whoami.test.ts +0 -99
  182. package/.claude/worktrees/emails-list/tests/helpers.ts +0 -93
  183. package/.claude/worktrees/emails-list/tests/lib/client.test.ts +0 -71
  184. package/.claude/worktrees/emails-list/tests/lib/config.test.ts +0 -414
  185. package/.claude/worktrees/emails-list/tests/lib/files.test.ts +0 -65
  186. package/.claude/worktrees/emails-list/tests/lib/help-text.test.ts +0 -97
  187. package/.claude/worktrees/emails-list/tests/lib/output.test.ts +0 -127
  188. package/.claude/worktrees/emails-list/tests/lib/prompts.test.ts +0 -178
  189. package/.claude/worktrees/emails-list/tests/lib/spinner.test.ts +0 -146
  190. package/.claude/worktrees/emails-list/tests/lib/table.test.ts +0 -63
  191. package/.claude/worktrees/emails-list/tests/lib/tty.test.ts +0 -85
  192. package/.claude/worktrees/emails-list/tsconfig.json +0 -14
  193. package/.github/workflows/test-build-windows.yml +0 -44
@@ -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
+ }
@@ -117,7 +117,6 @@ describe('login command', () => {
117
117
  expect(data.teams.production.api_key).toBe('re_old_key_1234');
118
118
  });
119
119
 
120
- // This test must be last — addCommand permanently modifies the shared loginCommand singleton
121
120
  test('auto-switches to team specified via --team flag', async () => {
122
121
  spies = setupOutputSpies();
123
122
 
@@ -146,6 +145,9 @@ describe('login command', () => {
146
145
  { from: 'user' },
147
146
  );
148
147
 
148
+ // @ts-expect-error — reset parent to avoid polluting the shared singleton
149
+ loginCommand.parent = null;
150
+
149
151
  const configPath = join(tmpDir, 'resend', 'credentials.json');
150
152
  const data = JSON.parse(readFileSync(configPath, 'utf-8'));
151
153
  expect(data.active_team).toBe('staging');
@@ -420,15 +420,13 @@ describe('validateTeamName', () => {
420
420
  expect(validateTeamName('my-team')).toBeUndefined();
421
421
  expect(validateTeamName('team_1')).toBeUndefined();
422
422
  expect(validateTeamName('prod-2024')).toBeUndefined();
423
- });
424
-
425
- test('rejects uppercase characters', () => {
426
- expect(validateTeamName('Production')).toContain('lowercase');
423
+ expect(validateTeamName('Production')).toBeUndefined();
424
+ expect(validateTeamName('MyTeam')).toBeUndefined();
427
425
  });
428
426
 
429
427
  test('rejects spaces and special characters', () => {
430
- expect(validateTeamName('my team')).toContain('lowercase');
431
- expect(validateTeamName('team@org')).toContain('lowercase');
428
+ expect(validateTeamName('my team')).toContain('letters');
429
+ expect(validateTeamName('team@org')).toContain('letters');
432
430
  });
433
431
 
434
432
  test('rejects empty name', () => {
@@ -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,5 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": ["Bash(npx biome:*)"]
4
- }
5
- }
@@ -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
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2025 Resend
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.