resend-cli 1.1.0 → 1.2.0

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 (205) hide show
  1. package/.claude/settings.local.json +1 -10
  2. package/.claude/worktrees/emails-list/.claude/settings.local.json +5 -0
  3. package/.claude/worktrees/emails-list/.github/scripts/pr-title-check.js +34 -0
  4. package/.claude/worktrees/emails-list/.github/workflows/ci.yml +32 -0
  5. package/.claude/worktrees/emails-list/.github/workflows/pr-title-check.yml +13 -0
  6. package/.claude/worktrees/emails-list/.github/workflows/release.yml +93 -0
  7. package/.claude/worktrees/emails-list/CHANGELOG.md +31 -0
  8. package/.claude/worktrees/emails-list/LICENSE +21 -0
  9. package/.claude/worktrees/emails-list/README.md +424 -0
  10. package/.claude/worktrees/emails-list/biome.json +36 -0
  11. package/.claude/worktrees/emails-list/bun.lock +76 -0
  12. package/.claude/worktrees/emails-list/bunfig.toml +2 -0
  13. package/.claude/worktrees/emails-list/install.ps1 +140 -0
  14. package/.claude/worktrees/emails-list/install.sh +301 -0
  15. package/.claude/worktrees/emails-list/package.json +43 -0
  16. package/.claude/worktrees/emails-list/renovate.json +6 -0
  17. package/.claude/worktrees/emails-list/src/cli.ts +74 -0
  18. package/.claude/worktrees/emails-list/src/commands/api-keys/create.ts +114 -0
  19. package/.claude/worktrees/emails-list/src/commands/api-keys/delete.ts +47 -0
  20. package/.claude/worktrees/emails-list/src/commands/api-keys/index.ts +26 -0
  21. package/.claude/worktrees/emails-list/src/commands/api-keys/list.ts +35 -0
  22. package/.claude/worktrees/emails-list/src/commands/api-keys/utils.ts +8 -0
  23. package/.claude/worktrees/emails-list/src/commands/auth/index.ts +20 -0
  24. package/.claude/worktrees/emails-list/src/commands/auth/login.ts +207 -0
  25. package/.claude/worktrees/emails-list/src/commands/auth/logout.ts +105 -0
  26. package/.claude/worktrees/emails-list/src/commands/broadcasts/create.ts +196 -0
  27. package/.claude/worktrees/emails-list/src/commands/broadcasts/delete.ts +46 -0
  28. package/.claude/worktrees/emails-list/src/commands/broadcasts/get.ts +59 -0
  29. package/.claude/worktrees/emails-list/src/commands/broadcasts/index.ts +43 -0
  30. package/.claude/worktrees/emails-list/src/commands/broadcasts/list.ts +60 -0
  31. package/.claude/worktrees/emails-list/src/commands/broadcasts/send.ts +56 -0
  32. package/.claude/worktrees/emails-list/src/commands/broadcasts/update.ts +95 -0
  33. package/.claude/worktrees/emails-list/src/commands/broadcasts/utils.ts +35 -0
  34. package/.claude/worktrees/emails-list/src/commands/contact-properties/create.ts +118 -0
  35. package/.claude/worktrees/emails-list/src/commands/contact-properties/delete.ts +48 -0
  36. package/.claude/worktrees/emails-list/src/commands/contact-properties/get.ts +46 -0
  37. package/.claude/worktrees/emails-list/src/commands/contact-properties/index.ts +48 -0
  38. package/.claude/worktrees/emails-list/src/commands/contact-properties/list.ts +68 -0
  39. package/.claude/worktrees/emails-list/src/commands/contact-properties/update.ts +88 -0
  40. package/.claude/worktrees/emails-list/src/commands/contact-properties/utils.ts +17 -0
  41. package/.claude/worktrees/emails-list/src/commands/contacts/add-segment.ts +78 -0
  42. package/.claude/worktrees/emails-list/src/commands/contacts/create.ts +122 -0
  43. package/.claude/worktrees/emails-list/src/commands/contacts/delete.ts +49 -0
  44. package/.claude/worktrees/emails-list/src/commands/contacts/get.ts +53 -0
  45. package/.claude/worktrees/emails-list/src/commands/contacts/index.ts +58 -0
  46. package/.claude/worktrees/emails-list/src/commands/contacts/list.ts +57 -0
  47. package/.claude/worktrees/emails-list/src/commands/contacts/remove-segment.ts +48 -0
  48. package/.claude/worktrees/emails-list/src/commands/contacts/segments.ts +39 -0
  49. package/.claude/worktrees/emails-list/src/commands/contacts/topics.ts +45 -0
  50. package/.claude/worktrees/emails-list/src/commands/contacts/update-topics.ts +90 -0
  51. package/.claude/worktrees/emails-list/src/commands/contacts/update.ts +77 -0
  52. package/.claude/worktrees/emails-list/src/commands/contacts/utils.ts +119 -0
  53. package/.claude/worktrees/emails-list/src/commands/doctor.ts +298 -0
  54. package/.claude/worktrees/emails-list/src/commands/domains/create.ts +83 -0
  55. package/.claude/worktrees/emails-list/src/commands/domains/delete.ts +42 -0
  56. package/.claude/worktrees/emails-list/src/commands/domains/get.ts +47 -0
  57. package/.claude/worktrees/emails-list/src/commands/domains/index.ts +35 -0
  58. package/.claude/worktrees/emails-list/src/commands/domains/list.ts +53 -0
  59. package/.claude/worktrees/emails-list/src/commands/domains/update.ts +75 -0
  60. package/.claude/worktrees/emails-list/src/commands/domains/utils.ts +44 -0
  61. package/.claude/worktrees/emails-list/src/commands/domains/verify.ts +38 -0
  62. package/.claude/worktrees/emails-list/src/commands/emails/batch.ts +140 -0
  63. package/.claude/worktrees/emails-list/src/commands/emails/index.ts +28 -0
  64. package/.claude/worktrees/emails-list/src/commands/emails/list.ts +73 -0
  65. package/.claude/worktrees/emails-list/src/commands/emails/receiving/attachment.ts +55 -0
  66. package/.claude/worktrees/emails-list/src/commands/emails/receiving/attachments.ts +68 -0
  67. package/.claude/worktrees/emails-list/src/commands/emails/receiving/get.ts +58 -0
  68. package/.claude/worktrees/emails-list/src/commands/emails/receiving/index.ts +28 -0
  69. package/.claude/worktrees/emails-list/src/commands/emails/receiving/list.ts +59 -0
  70. package/.claude/worktrees/emails-list/src/commands/emails/receiving/utils.ts +38 -0
  71. package/.claude/worktrees/emails-list/src/commands/emails/send.ts +189 -0
  72. package/.claude/worktrees/emails-list/src/commands/open.ts +24 -0
  73. package/.claude/worktrees/emails-list/src/commands/segments/create.ts +50 -0
  74. package/.claude/worktrees/emails-list/src/commands/segments/delete.ts +47 -0
  75. package/.claude/worktrees/emails-list/src/commands/segments/get.ts +38 -0
  76. package/.claude/worktrees/emails-list/src/commands/segments/index.ts +36 -0
  77. package/.claude/worktrees/emails-list/src/commands/segments/list.ts +58 -0
  78. package/.claude/worktrees/emails-list/src/commands/segments/utils.ts +7 -0
  79. package/.claude/worktrees/emails-list/src/commands/teams/index.ts +10 -0
  80. package/.claude/worktrees/emails-list/src/commands/teams/list.ts +35 -0
  81. package/.claude/worktrees/emails-list/src/commands/teams/remove.ts +83 -0
  82. package/.claude/worktrees/emails-list/src/commands/teams/switch.ts +73 -0
  83. package/.claude/worktrees/emails-list/src/commands/topics/create.ts +73 -0
  84. package/.claude/worktrees/emails-list/src/commands/topics/delete.ts +47 -0
  85. package/.claude/worktrees/emails-list/src/commands/topics/get.ts +42 -0
  86. package/.claude/worktrees/emails-list/src/commands/topics/index.ts +42 -0
  87. package/.claude/worktrees/emails-list/src/commands/topics/list.ts +34 -0
  88. package/.claude/worktrees/emails-list/src/commands/topics/update.ts +59 -0
  89. package/.claude/worktrees/emails-list/src/commands/topics/utils.ts +16 -0
  90. package/.claude/worktrees/emails-list/src/commands/webhooks/create.ts +128 -0
  91. package/.claude/worktrees/emails-list/src/commands/webhooks/delete.ts +49 -0
  92. package/.claude/worktrees/emails-list/src/commands/webhooks/get.ts +42 -0
  93. package/.claude/worktrees/emails-list/src/commands/webhooks/index.ts +44 -0
  94. package/.claude/worktrees/emails-list/src/commands/webhooks/list.ts +55 -0
  95. package/.claude/worktrees/emails-list/src/commands/webhooks/update.ts +83 -0
  96. package/.claude/worktrees/emails-list/src/commands/webhooks/utils.ts +36 -0
  97. package/.claude/worktrees/emails-list/src/commands/whoami.ts +71 -0
  98. package/.claude/worktrees/emails-list/src/lib/actions.ts +157 -0
  99. package/.claude/worktrees/emails-list/src/lib/client.ts +34 -0
  100. package/.claude/worktrees/emails-list/src/lib/config.ts +211 -0
  101. package/.claude/worktrees/emails-list/src/lib/files.ts +15 -0
  102. package/.claude/worktrees/emails-list/src/lib/help-text.ts +38 -0
  103. package/.claude/worktrees/emails-list/src/lib/output.ts +54 -0
  104. package/.claude/worktrees/emails-list/src/lib/pagination.ts +36 -0
  105. package/.claude/worktrees/emails-list/src/lib/prompts.ts +149 -0
  106. package/.claude/worktrees/emails-list/src/lib/spinner.ts +93 -0
  107. package/.claude/worktrees/emails-list/src/lib/table.ts +57 -0
  108. package/.claude/worktrees/emails-list/src/lib/tty.ts +28 -0
  109. package/.claude/worktrees/emails-list/src/lib/version.ts +4 -0
  110. package/.claude/worktrees/emails-list/tests/commands/api-keys/create.test.ts +195 -0
  111. package/.claude/worktrees/emails-list/tests/commands/api-keys/delete.test.ts +156 -0
  112. package/.claude/worktrees/emails-list/tests/commands/api-keys/list.test.ts +133 -0
  113. package/.claude/worktrees/emails-list/tests/commands/auth/login.test.ts +119 -0
  114. package/.claude/worktrees/emails-list/tests/commands/auth/logout.test.ts +146 -0
  115. package/.claude/worktrees/emails-list/tests/commands/broadcasts/create.test.ts +447 -0
  116. package/.claude/worktrees/emails-list/tests/commands/broadcasts/delete.test.ts +182 -0
  117. package/.claude/worktrees/emails-list/tests/commands/broadcasts/get.test.ts +146 -0
  118. package/.claude/worktrees/emails-list/tests/commands/broadcasts/list.test.ts +196 -0
  119. package/.claude/worktrees/emails-list/tests/commands/broadcasts/send.test.ts +161 -0
  120. package/.claude/worktrees/emails-list/tests/commands/broadcasts/update.test.ts +283 -0
  121. package/.claude/worktrees/emails-list/tests/commands/contact-properties/create.test.ts +250 -0
  122. package/.claude/worktrees/emails-list/tests/commands/contact-properties/delete.test.ts +183 -0
  123. package/.claude/worktrees/emails-list/tests/commands/contact-properties/get.test.ts +144 -0
  124. package/.claude/worktrees/emails-list/tests/commands/contact-properties/list.test.ts +180 -0
  125. package/.claude/worktrees/emails-list/tests/commands/contact-properties/update.test.ts +216 -0
  126. package/.claude/worktrees/emails-list/tests/commands/contacts/add-segment.test.ts +188 -0
  127. package/.claude/worktrees/emails-list/tests/commands/contacts/create.test.ts +270 -0
  128. package/.claude/worktrees/emails-list/tests/commands/contacts/delete.test.ts +192 -0
  129. package/.claude/worktrees/emails-list/tests/commands/contacts/get.test.ts +148 -0
  130. package/.claude/worktrees/emails-list/tests/commands/contacts/list.test.ts +175 -0
  131. package/.claude/worktrees/emails-list/tests/commands/contacts/remove-segment.test.ts +166 -0
  132. package/.claude/worktrees/emails-list/tests/commands/contacts/segments.test.ts +167 -0
  133. package/.claude/worktrees/emails-list/tests/commands/contacts/topics.test.ts +163 -0
  134. package/.claude/worktrees/emails-list/tests/commands/contacts/update-topics.test.ts +247 -0
  135. package/.claude/worktrees/emails-list/tests/commands/contacts/update.test.ts +205 -0
  136. package/.claude/worktrees/emails-list/tests/commands/doctor.test.ts +165 -0
  137. package/.claude/worktrees/emails-list/tests/commands/domains/create.test.ts +192 -0
  138. package/.claude/worktrees/emails-list/tests/commands/domains/delete.test.ts +156 -0
  139. package/.claude/worktrees/emails-list/tests/commands/domains/get.test.ts +137 -0
  140. package/.claude/worktrees/emails-list/tests/commands/domains/list.test.ts +164 -0
  141. package/.claude/worktrees/emails-list/tests/commands/domains/update.test.ts +223 -0
  142. package/.claude/worktrees/emails-list/tests/commands/domains/verify.test.ts +117 -0
  143. package/.claude/worktrees/emails-list/tests/commands/emails/batch.test.ts +313 -0
  144. package/.claude/worktrees/emails-list/tests/commands/emails/list.test.ts +196 -0
  145. package/.claude/worktrees/emails-list/tests/commands/emails/receiving/attachment.test.ts +140 -0
  146. package/.claude/worktrees/emails-list/tests/commands/emails/receiving/attachments.test.ts +168 -0
  147. package/.claude/worktrees/emails-list/tests/commands/emails/receiving/get.test.ts +140 -0
  148. package/.claude/worktrees/emails-list/tests/commands/emails/receiving/list.test.ts +181 -0
  149. package/.claude/worktrees/emails-list/tests/commands/emails/send.test.ts +309 -0
  150. package/.claude/worktrees/emails-list/tests/commands/segments/create.test.ts +163 -0
  151. package/.claude/worktrees/emails-list/tests/commands/segments/delete.test.ts +182 -0
  152. package/.claude/worktrees/emails-list/tests/commands/segments/get.test.ts +137 -0
  153. package/.claude/worktrees/emails-list/tests/commands/segments/list.test.ts +173 -0
  154. package/.claude/worktrees/emails-list/tests/commands/teams/list.test.ts +63 -0
  155. package/.claude/worktrees/emails-list/tests/commands/teams/remove.test.ts +103 -0
  156. package/.claude/worktrees/emails-list/tests/commands/teams/switch.test.ts +96 -0
  157. package/.claude/worktrees/emails-list/tests/commands/topics/create.test.ts +191 -0
  158. package/.claude/worktrees/emails-list/tests/commands/topics/delete.test.ts +156 -0
  159. package/.claude/worktrees/emails-list/tests/commands/topics/get.test.ts +125 -0
  160. package/.claude/worktrees/emails-list/tests/commands/topics/list.test.ts +124 -0
  161. package/.claude/worktrees/emails-list/tests/commands/topics/update.test.ts +177 -0
  162. package/.claude/worktrees/emails-list/tests/commands/webhooks/create.test.ts +224 -0
  163. package/.claude/worktrees/emails-list/tests/commands/webhooks/delete.test.ts +156 -0
  164. package/.claude/worktrees/emails-list/tests/commands/webhooks/get.test.ts +125 -0
  165. package/.claude/worktrees/emails-list/tests/commands/webhooks/list.test.ts +177 -0
  166. package/.claude/worktrees/emails-list/tests/commands/webhooks/update.test.ts +206 -0
  167. package/.claude/worktrees/emails-list/tests/commands/whoami.test.ts +99 -0
  168. package/.claude/worktrees/emails-list/tests/helpers.ts +93 -0
  169. package/.claude/worktrees/emails-list/tests/lib/client.test.ts +71 -0
  170. package/.claude/worktrees/emails-list/tests/lib/config.test.ts +414 -0
  171. package/.claude/worktrees/emails-list/tests/lib/files.test.ts +65 -0
  172. package/.claude/worktrees/emails-list/tests/lib/help-text.test.ts +97 -0
  173. package/.claude/worktrees/emails-list/tests/lib/output.test.ts +127 -0
  174. package/.claude/worktrees/emails-list/tests/lib/prompts.test.ts +178 -0
  175. package/.claude/worktrees/emails-list/tests/lib/spinner.test.ts +146 -0
  176. package/.claude/worktrees/emails-list/tests/lib/table.test.ts +63 -0
  177. package/.claude/worktrees/emails-list/tests/lib/tty.test.ts +85 -0
  178. package/.claude/worktrees/emails-list/tsconfig.json +14 -0
  179. package/.github/workflows/ci.yml +3 -3
  180. package/.github/workflows/pr-title-check.yml +1 -1
  181. package/.github/workflows/release.yml +2 -2
  182. package/.github/workflows/test-build-windows.yml +44 -0
  183. package/.github/workflows/test-install-windows.yml +48 -0
  184. package/README.md +8 -0
  185. package/docs/agent-dx-gaps.md +167 -0
  186. package/docs/missing-commands.md +58 -0
  187. package/docs/production-readiness.md +99 -0
  188. package/docs/secure-key-storage.md +174 -0
  189. package/install.ps1 +1 -0
  190. package/install.sh +11 -4
  191. package/package.json +1 -1
  192. package/renovate.json +4 -0
  193. package/src/cli.ts +9 -0
  194. package/src/commands/auth/login.ts +34 -13
  195. package/src/commands/doctor.ts +3 -3
  196. package/src/commands/open.ts +24 -0
  197. package/src/commands/teams/remove.ts +5 -2
  198. package/src/commands/teams/switch.ts +3 -0
  199. package/src/lib/client.ts +6 -1
  200. package/src/lib/config.ts +37 -30
  201. package/src/lib/help-text.ts +4 -2
  202. package/src/lib/spinner.ts +7 -3
  203. package/tests/commands/auth/login.test.ts +35 -0
  204. package/tests/lib/config.test.ts +40 -7
  205. package/tests/lib/help-text.test.ts +2 -1
@@ -3,7 +3,13 @@ import * as p from '@clack/prompts';
3
3
  import { Command } from '@commander-js/extra-typings';
4
4
  import { Resend } from 'resend';
5
5
  import type { GlobalOpts } from '../../lib/client';
6
- import { listTeams, resolveApiKey, storeApiKey } from '../../lib/config';
6
+ import {
7
+ listTeams,
8
+ resolveApiKey,
9
+ setActiveTeam,
10
+ storeApiKey,
11
+ validateTeamName,
12
+ } from '../../lib/config';
7
13
  import { buildHelpText } from '../../lib/help-text';
8
14
  import { errorMessage, outputError, outputResult } from '../../lib/output';
9
15
  import { cancelAndExit } from '../../lib/prompts';
@@ -127,7 +133,11 @@ export const loginCommand = new Command('login')
127
133
  );
128
134
  }
129
135
 
130
- const spinner = createSpinner('Validating API key...');
136
+ const spinner = createSpinner(
137
+ 'Validating API key...',
138
+ 'braille',
139
+ globalOpts.quiet,
140
+ );
131
141
 
132
142
  try {
133
143
  const resend = new Resend(apiKey);
@@ -146,6 +156,17 @@ export const loginCommand = new Command('login')
146
156
 
147
157
  let teamName = globalOpts.team;
148
158
 
159
+ if (teamName) {
160
+ const teamError = validateTeamName(teamName);
161
+ if (teamError) {
162
+ outputError(
163
+ { message: teamError, code: 'invalid_team_name' },
164
+ { json: globalOpts.json },
165
+ );
166
+ return;
167
+ }
168
+ }
169
+
149
170
  if (!teamName && isInteractive()) {
150
171
  const existingTeams = listTeams();
151
172
  if (existingTeams.length > 0) {
@@ -169,8 +190,7 @@ export const loginCommand = new Command('login')
169
190
  if (choice === '__new__') {
170
191
  const newName = await p.text({
171
192
  message: 'Enter a name for the new team:',
172
- validate: (v) =>
173
- !v || v.length === 0 ? 'Team name is required' : undefined,
193
+ validate: (v) => validateTeamName(v),
174
194
  });
175
195
  if (p.isCancel(newName)) {
176
196
  cancelAndExit('Login cancelled.');
@@ -180,21 +200,22 @@ export const loginCommand = new Command('login')
180
200
  teamName = choice;
181
201
  }
182
202
  } else {
183
- const newName = await p.text({
184
- message: 'Enter a team name (or press Enter for "default"):',
185
- defaultValue: 'default',
186
- placeholder: 'default',
187
- });
188
- if (p.isCancel(newName)) {
189
- cancelAndExit('Login cancelled.');
190
- }
191
- teamName = newName;
203
+ teamName = 'default';
192
204
  }
193
205
  }
194
206
 
195
207
  const configPath = storeApiKey(apiKey, teamName);
196
208
  const teamLabel = teamName || 'default';
197
209
 
210
+ // Auto-switch to the newly added team
211
+ if (teamName) {
212
+ try {
213
+ setActiveTeam(teamName);
214
+ } catch {
215
+ // Team was just stored, so this should not fail
216
+ }
217
+ }
218
+
198
219
  if (globalOpts.json) {
199
220
  outputResult(
200
221
  { success: true, config_path: configPath, team: teamLabel },
@@ -66,8 +66,8 @@ async function checkCliVersion(): Promise<CheckResult> {
66
66
  }
67
67
  }
68
68
 
69
- function checkApiKeyPresence(): CheckResult {
70
- const resolved = resolveApiKey();
69
+ function checkApiKeyPresence(flagValue?: string): CheckResult {
70
+ const resolved = resolveApiKey(flagValue);
71
71
  if (!resolved) {
72
72
  return {
73
73
  name: 'API Key',
@@ -246,7 +246,7 @@ export const doctorCommand = new Command('doctor')
246
246
 
247
247
  // Check 2: API Key
248
248
  spinner = interactive ? createSpinner('Checking API key...', 'scan') : null;
249
- const keyCheck = checkApiKeyPresence();
249
+ const keyCheck = checkApiKeyPresence(globalOpts.apiKey);
250
250
  checks.push(keyCheck);
251
251
  if (keyCheck.status === 'fail') {
252
252
  spinner?.fail(keyCheck.message);
@@ -0,0 +1,24 @@
1
+ import { Command } from '@commander-js/extra-typings';
2
+ import { buildHelpText } from '../lib/help-text';
3
+
4
+ export const openCommand = new Command('open')
5
+ .description('Open the Resend dashboard in your browser')
6
+ .addHelpText(
7
+ 'after',
8
+ buildHelpText({
9
+ context: 'Opens https://resend.com/emails in your default browser.',
10
+ examples: ['resend open'],
11
+ }),
12
+ )
13
+ .action(async () => {
14
+ const url = 'https://resend.com/emails';
15
+ const { platform } = process;
16
+ const args =
17
+ platform === 'darwin'
18
+ ? ['open', url]
19
+ : platform === 'win32'
20
+ ? ['cmd', '/c', 'start', url]
21
+ : ['xdg-open', url];
22
+
23
+ Bun.spawn(args, { stdio: ['ignore', 'ignore', 'ignore'] });
24
+ });
@@ -1,7 +1,7 @@
1
1
  import * as p from '@clack/prompts';
2
2
  import { Command } from '@commander-js/extra-typings';
3
3
  import type { GlobalOpts } from '../../lib/client';
4
- import { listTeams, removeTeam } from '../../lib/config';
4
+ import { listTeams, removeApiKey } from '../../lib/config';
5
5
  import { errorMessage, outputError, outputResult } from '../../lib/output';
6
6
  import { cancelAndExit } from '../../lib/prompts';
7
7
  import { isInteractive } from '../../lib/tty';
@@ -24,6 +24,7 @@ export const removeCommand = new Command('remove')
24
24
  },
25
25
  { json: globalOpts.json },
26
26
  );
27
+ return;
27
28
  }
28
29
 
29
30
  const teams = listTeams();
@@ -35,6 +36,7 @@ export const removeCommand = new Command('remove')
35
36
  },
36
37
  { json: globalOpts.json },
37
38
  );
39
+ return;
38
40
  }
39
41
 
40
42
  const choice = await p.select({
@@ -64,7 +66,7 @@ export const removeCommand = new Command('remove')
64
66
  }
65
67
 
66
68
  try {
67
- removeTeam(teamName);
69
+ removeApiKey(teamName);
68
70
  } catch (err) {
69
71
  outputError(
70
72
  {
@@ -73,6 +75,7 @@ export const removeCommand = new Command('remove')
73
75
  },
74
76
  { json: globalOpts.json },
75
77
  );
78
+ return;
76
79
  }
77
80
 
78
81
  if (globalOpts.json) {
@@ -24,6 +24,7 @@ export const switchCommand = new Command('switch')
24
24
  },
25
25
  { json: globalOpts.json },
26
26
  );
27
+ return;
27
28
  }
28
29
 
29
30
  const teams = listTeams();
@@ -35,6 +36,7 @@ export const switchCommand = new Command('switch')
35
36
  },
36
37
  { json: globalOpts.json },
37
38
  );
39
+ return;
38
40
  }
39
41
 
40
42
  const choice = await p.select({
@@ -63,6 +65,7 @@ export const switchCommand = new Command('switch')
63
65
  },
64
66
  { json: globalOpts.json },
65
67
  );
68
+ return;
66
69
  }
67
70
 
68
71
  if (globalOpts.json) {
package/src/lib/client.ts CHANGED
@@ -2,7 +2,12 @@ import { Resend } from 'resend';
2
2
  import { resolveApiKey } from './config';
3
3
  import { errorMessage, outputError } from './output';
4
4
 
5
- export type GlobalOpts = { apiKey?: string; json?: boolean; team?: string };
5
+ export type GlobalOpts = {
6
+ apiKey?: string;
7
+ json?: boolean;
8
+ quiet?: boolean;
9
+ team?: string;
10
+ };
6
11
 
7
12
  export function createClient(flagValue?: string, teamName?: string): Resend {
8
13
  const resolved = resolveApiKey(flagValue, teamName);
package/src/lib/config.ts CHANGED
@@ -1,4 +1,11 @@
1
- import { chmodSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
1
+ import {
2
+ chmodSync,
3
+ existsSync,
4
+ mkdirSync,
5
+ readFileSync,
6
+ unlinkSync,
7
+ writeFileSync,
8
+ } from 'node:fs';
2
9
  import { homedir } from 'node:os';
3
10
  import { join } from 'node:path';
4
11
 
@@ -102,6 +109,10 @@ export function resolveApiKey(
102
109
 
103
110
  export function storeApiKey(apiKey: string, teamName?: string): string {
104
111
  const team = teamName || 'default';
112
+ const validationError = validateTeamName(team);
113
+ if (validationError) {
114
+ throw new Error(validationError);
115
+ }
105
116
  const creds = readCredentials() || { active_team: 'default', teams: {} };
106
117
 
107
118
  creds.teams[team] = { api_key: apiKey };
@@ -116,7 +127,6 @@ export function storeApiKey(apiKey: string, teamName?: string): string {
116
127
 
117
128
  export function removeAllApiKeys(): string {
118
129
  const configPath = getCredentialsPath();
119
- const { unlinkSync } = require('node:fs');
120
130
  unlinkSync(configPath);
121
131
  return configPath;
122
132
  }
@@ -125,13 +135,20 @@ export function removeApiKey(teamName?: string): string {
125
135
  const creds = readCredentials();
126
136
  if (!creds) {
127
137
  const configPath = getCredentialsPath();
138
+ if (!existsSync(configPath)) {
139
+ throw new Error('No credentials file found.');
140
+ }
128
141
  // Try to delete legacy file
129
- const { unlinkSync } = require('node:fs');
130
142
  unlinkSync(configPath);
131
143
  return configPath;
132
144
  }
133
145
 
134
146
  const team = teamName || resolveTeamName();
147
+ if (!creds.teams[team]) {
148
+ throw new Error(
149
+ `Team "${team}" not found. Available teams: ${Object.keys(creds.teams).join(', ')}`,
150
+ );
151
+ }
135
152
  delete creds.teams[team];
136
153
 
137
154
  // If we removed the active team, switch to first available or "default"
@@ -142,7 +159,6 @@ export function removeApiKey(teamName?: string): string {
142
159
 
143
160
  // If no teams left, delete the file
144
161
  if (Object.keys(creds.teams).length === 0) {
145
- const { unlinkSync } = require('node:fs');
146
162
  const configPath = getCredentialsPath();
147
163
  unlinkSync(configPath);
148
164
  return configPath;
@@ -152,6 +168,10 @@ export function removeApiKey(teamName?: string): string {
152
168
  }
153
169
 
154
170
  export function setActiveTeam(teamName: string): void {
171
+ const validationError = validateTeamName(teamName);
172
+ if (validationError) {
173
+ throw new Error(validationError);
174
+ }
155
175
  const creds = readCredentials();
156
176
  if (!creds) {
157
177
  throw new Error('No credentials file found. Run: resend login');
@@ -176,6 +196,19 @@ export function listTeams(): Array<{ name: string; active: boolean }> {
176
196
  }));
177
197
  }
178
198
 
199
+ export function validateTeamName(name: string): string | undefined {
200
+ if (!name || name.length === 0) {
201
+ return 'Team name must not be empty';
202
+ }
203
+ if (name.length > 64) {
204
+ return 'Team name must be 64 characters or fewer';
205
+ }
206
+ if (!/^[a-z0-9_-]+$/.test(name)) {
207
+ return 'Team name must contain only lowercase letters, numbers, dashes, and underscores';
208
+ }
209
+ return undefined;
210
+ }
211
+
179
212
  export function maskKey(key: string): string {
180
213
  if (key.length <= 7) {
181
214
  return `${key.slice(0, 3)}...`;
@@ -183,29 +216,3 @@ export function maskKey(key: string): string {
183
216
  return `${key.slice(0, 3)}...${key.slice(-4)}`;
184
217
  }
185
218
 
186
- export function removeTeam(teamName: string): void {
187
- const creds = readCredentials();
188
- if (!creds) {
189
- throw new Error('No credentials file found.');
190
- }
191
- if (!creds.teams[teamName]) {
192
- throw new Error(
193
- `Team "${teamName}" not found. Available teams: ${Object.keys(creds.teams).join(', ')}`,
194
- );
195
- }
196
-
197
- delete creds.teams[teamName];
198
-
199
- if (creds.active_team === teamName) {
200
- const remaining = Object.keys(creds.teams);
201
- creds.active_team = remaining[0] || 'default';
202
- }
203
-
204
- if (Object.keys(creds.teams).length === 0) {
205
- const { unlinkSync } = require('node:fs');
206
- unlinkSync(getCredentialsPath());
207
- return;
208
- }
209
-
210
- writeCredentials(creds);
211
- }
@@ -9,11 +9,13 @@ export interface HelpTextOptions {
9
9
  const GLOBAL_OPTS_FULL = `Global options:
10
10
  --api-key <key> API key (or set RESEND_API_KEY env var)
11
11
  --team <name> Team profile to use (overrides RESEND_TEAM)
12
- --json Force JSON output (also auto-enabled when stdout is piped)`;
12
+ --json Force JSON output (also auto-enabled when stdout is piped)
13
+ -q, --quiet Suppress spinners and status output (implies --json)`;
13
14
 
14
15
  const GLOBAL_OPTS_SETUP = `Global options:
15
16
  --team <name> Team profile to save the key to
16
- --json Force JSON output`;
17
+ --json Force JSON output
18
+ -q, --quiet Suppress spinners and status output (implies --json)`;
17
19
 
18
20
  const ERROR_ENVELOPE = ` {"error":{"message":"<message>","code":"<code>"}}`;
19
21
 
@@ -21,7 +21,7 @@ export async function withSpinner<T>(
21
21
  errorCode: string,
22
22
  globalOpts: GlobalOpts,
23
23
  ): Promise<T> {
24
- const spinner = createSpinner(messages.loading);
24
+ const spinner = createSpinner(messages.loading, 'braille', globalOpts.quiet);
25
25
  try {
26
26
  const { data, error } = await call();
27
27
  if (error) {
@@ -51,8 +51,12 @@ export async function withSpinner<T>(
51
51
 
52
52
  export type SpinnerName = keyof typeof spinners;
53
53
 
54
- export function createSpinner(message: string, name: SpinnerName = 'braille') {
55
- if (!isInteractive()) {
54
+ export function createSpinner(
55
+ message: string,
56
+ name: SpinnerName = 'braille',
57
+ quiet?: boolean,
58
+ ) {
59
+ if (quiet || !isInteractive()) {
56
60
  return {
57
61
  update(_msg: string) {},
58
62
  stop(_msg: string) {},
@@ -116,4 +116,39 @@ 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
+ // This test must be last — addCommand permanently modifies the shared loginCommand singleton
121
+ test('auto-switches to team specified via --team flag', async () => {
122
+ spies = setupOutputSpies();
123
+
124
+ const { Command } = await import('@commander-js/extra-typings');
125
+ const { loginCommand } = await import('../../../src/commands/auth/login');
126
+ const program = new Command()
127
+ .option('--team <name>')
128
+ .option('--json')
129
+ .option('--api-key <key>')
130
+ .option('-q, --quiet')
131
+ .addCommand(loginCommand);
132
+
133
+ // First store a default key
134
+ const configDir = join(tmpDir, 'resend');
135
+ mkdirSync(configDir, { recursive: true });
136
+ writeFileSync(
137
+ join(configDir, 'credentials.json'),
138
+ JSON.stringify({
139
+ active_team: 'default',
140
+ teams: { default: { api_key: 're_old_key_1234' } },
141
+ }),
142
+ );
143
+
144
+ await program.parseAsync(
145
+ ['login', '--key', 're_staging_key_123', '--team', 'staging'],
146
+ { from: 'user' },
147
+ );
148
+
149
+ const configPath = join(tmpDir, 'resend', 'credentials.json');
150
+ const data = JSON.parse(readFileSync(configPath, 'utf-8'));
151
+ expect(data.active_team).toBe('staging');
152
+ expect(data.teams.staging.api_key).toBe('re_staging_key_123');
153
+ });
119
154
  });
@@ -5,11 +5,12 @@ import { join } from 'node:path';
5
5
  import {
6
6
  getConfigDir,
7
7
  listTeams,
8
- removeTeam,
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('removeTeam', () => {
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
- removeTeam('staging');
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
- removeTeam('staging');
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
- removeTeam('only');
400
+ removeApiKey('only');
400
401
 
401
402
  const { existsSync } = require('node:fs');
402
403
  const configPath = join(tmpDir, 'resend', 'credentials.json');
@@ -405,10 +406,42 @@ describe('removeTeam', () => {
405
406
 
406
407
  test('throws when team does not exist', () => {
407
408
  storeApiKey('re_default');
408
- expect(() => removeTeam('nonexistent')).toThrow('not found');
409
+ expect(() => removeApiKey('nonexistent')).toThrow('not found');
409
410
  });
410
411
 
411
412
  test('throws when no credentials file', () => {
412
- expect(() => removeTeam('any')).toThrow('No credentials file');
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
+ });
424
+
425
+ test('rejects uppercase characters', () => {
426
+ expect(validateTeamName('Production')).toContain('lowercase');
427
+ });
428
+
429
+ test('rejects spaces and special characters', () => {
430
+ expect(validateTeamName('my team')).toContain('lowercase');
431
+ expect(validateTeamName('team@org')).toContain('lowercase');
432
+ });
433
+
434
+ test('rejects empty name', () => {
435
+ expect(validateTeamName('')).toContain('empty');
436
+ });
437
+
438
+ test('rejects names longer than 64 characters', () => {
439
+ const longName = 'a'.repeat(65);
440
+ expect(validateTeamName(longName)).toContain('64');
441
+ });
442
+
443
+ test('accepts name exactly 64 characters', () => {
444
+ const maxName = 'a'.repeat(64);
445
+ expect(validateTeamName(maxName)).toBeUndefined();
413
446
  });
414
447
  });
@@ -20,7 +20,8 @@ describe('buildHelpText', () => {
20
20
  'Global options:\n' +
21
21
  ' --api-key <key> API key (or set RESEND_API_KEY env var)\n' +
22
22
  ' --team <name> Team profile to use (overrides RESEND_TEAM)\n' +
23
- ' --json Force JSON output (also auto-enabled when stdout is piped)' +
23
+ ' --json Force JSON output (also auto-enabled when stdout is piped)\n' +
24
+ ' -q, --quiet Suppress spinners and status output (implies --json)' +
24
25
  '\n\n' +
25
26
  'Output (--json or piped):\n' +
26
27
  ' {"id":"em_123"}' +