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.
Files changed (191) hide show
  1. package/biome.json +1 -1
  2. package/bun.lock +0 -3
  3. package/package.json +2 -3
  4. package/src/cli.ts +11 -1
  5. package/src/commands/auth/login.ts +35 -8
  6. package/src/commands/doctor.ts +33 -115
  7. package/src/commands/teams/remove.ts +5 -2
  8. package/src/commands/teams/switch.ts +3 -0
  9. package/src/lib/config.ts +37 -31
  10. package/src/lib/spinner.ts +17 -10
  11. package/src/lib/update-check.ts +172 -0
  12. package/tests/commands/auth/login.test.ts +37 -0
  13. package/tests/lib/config.test.ts +38 -7
  14. package/tests/lib/update-check.test.ts +169 -0
  15. package/.claude/worktrees/emails-list/.claude/settings.local.json +0 -5
  16. package/.claude/worktrees/emails-list/.github/scripts/pr-title-check.js +0 -34
  17. package/.claude/worktrees/emails-list/.github/workflows/ci.yml +0 -32
  18. package/.claude/worktrees/emails-list/.github/workflows/pr-title-check.yml +0 -13
  19. package/.claude/worktrees/emails-list/.github/workflows/release.yml +0 -93
  20. package/.claude/worktrees/emails-list/CHANGELOG.md +0 -31
  21. package/.claude/worktrees/emails-list/LICENSE +0 -21
  22. package/.claude/worktrees/emails-list/README.md +0 -424
  23. package/.claude/worktrees/emails-list/biome.json +0 -36
  24. package/.claude/worktrees/emails-list/bun.lock +0 -76
  25. package/.claude/worktrees/emails-list/bunfig.toml +0 -2
  26. package/.claude/worktrees/emails-list/install.ps1 +0 -140
  27. package/.claude/worktrees/emails-list/install.sh +0 -301
  28. package/.claude/worktrees/emails-list/package.json +0 -43
  29. package/.claude/worktrees/emails-list/renovate.json +0 -6
  30. package/.claude/worktrees/emails-list/src/cli.ts +0 -74
  31. package/.claude/worktrees/emails-list/src/commands/api-keys/create.ts +0 -114
  32. package/.claude/worktrees/emails-list/src/commands/api-keys/delete.ts +0 -47
  33. package/.claude/worktrees/emails-list/src/commands/api-keys/index.ts +0 -26
  34. package/.claude/worktrees/emails-list/src/commands/api-keys/list.ts +0 -35
  35. package/.claude/worktrees/emails-list/src/commands/api-keys/utils.ts +0 -8
  36. package/.claude/worktrees/emails-list/src/commands/auth/index.ts +0 -20
  37. package/.claude/worktrees/emails-list/src/commands/auth/login.ts +0 -207
  38. package/.claude/worktrees/emails-list/src/commands/auth/logout.ts +0 -105
  39. package/.claude/worktrees/emails-list/src/commands/broadcasts/create.ts +0 -196
  40. package/.claude/worktrees/emails-list/src/commands/broadcasts/delete.ts +0 -46
  41. package/.claude/worktrees/emails-list/src/commands/broadcasts/get.ts +0 -59
  42. package/.claude/worktrees/emails-list/src/commands/broadcasts/index.ts +0 -43
  43. package/.claude/worktrees/emails-list/src/commands/broadcasts/list.ts +0 -60
  44. package/.claude/worktrees/emails-list/src/commands/broadcasts/send.ts +0 -56
  45. package/.claude/worktrees/emails-list/src/commands/broadcasts/update.ts +0 -95
  46. package/.claude/worktrees/emails-list/src/commands/broadcasts/utils.ts +0 -35
  47. package/.claude/worktrees/emails-list/src/commands/contact-properties/create.ts +0 -118
  48. package/.claude/worktrees/emails-list/src/commands/contact-properties/delete.ts +0 -48
  49. package/.claude/worktrees/emails-list/src/commands/contact-properties/get.ts +0 -46
  50. package/.claude/worktrees/emails-list/src/commands/contact-properties/index.ts +0 -48
  51. package/.claude/worktrees/emails-list/src/commands/contact-properties/list.ts +0 -68
  52. package/.claude/worktrees/emails-list/src/commands/contact-properties/update.ts +0 -88
  53. package/.claude/worktrees/emails-list/src/commands/contact-properties/utils.ts +0 -17
  54. package/.claude/worktrees/emails-list/src/commands/contacts/add-segment.ts +0 -78
  55. package/.claude/worktrees/emails-list/src/commands/contacts/create.ts +0 -122
  56. package/.claude/worktrees/emails-list/src/commands/contacts/delete.ts +0 -49
  57. package/.claude/worktrees/emails-list/src/commands/contacts/get.ts +0 -53
  58. package/.claude/worktrees/emails-list/src/commands/contacts/index.ts +0 -58
  59. package/.claude/worktrees/emails-list/src/commands/contacts/list.ts +0 -57
  60. package/.claude/worktrees/emails-list/src/commands/contacts/remove-segment.ts +0 -48
  61. package/.claude/worktrees/emails-list/src/commands/contacts/segments.ts +0 -39
  62. package/.claude/worktrees/emails-list/src/commands/contacts/topics.ts +0 -45
  63. package/.claude/worktrees/emails-list/src/commands/contacts/update-topics.ts +0 -90
  64. package/.claude/worktrees/emails-list/src/commands/contacts/update.ts +0 -77
  65. package/.claude/worktrees/emails-list/src/commands/contacts/utils.ts +0 -119
  66. package/.claude/worktrees/emails-list/src/commands/doctor.ts +0 -298
  67. package/.claude/worktrees/emails-list/src/commands/domains/create.ts +0 -83
  68. package/.claude/worktrees/emails-list/src/commands/domains/delete.ts +0 -42
  69. package/.claude/worktrees/emails-list/src/commands/domains/get.ts +0 -47
  70. package/.claude/worktrees/emails-list/src/commands/domains/index.ts +0 -35
  71. package/.claude/worktrees/emails-list/src/commands/domains/list.ts +0 -53
  72. package/.claude/worktrees/emails-list/src/commands/domains/update.ts +0 -75
  73. package/.claude/worktrees/emails-list/src/commands/domains/utils.ts +0 -44
  74. package/.claude/worktrees/emails-list/src/commands/domains/verify.ts +0 -38
  75. package/.claude/worktrees/emails-list/src/commands/emails/batch.ts +0 -140
  76. package/.claude/worktrees/emails-list/src/commands/emails/index.ts +0 -28
  77. package/.claude/worktrees/emails-list/src/commands/emails/list.ts +0 -73
  78. package/.claude/worktrees/emails-list/src/commands/emails/receiving/attachment.ts +0 -55
  79. package/.claude/worktrees/emails-list/src/commands/emails/receiving/attachments.ts +0 -68
  80. package/.claude/worktrees/emails-list/src/commands/emails/receiving/get.ts +0 -58
  81. package/.claude/worktrees/emails-list/src/commands/emails/receiving/index.ts +0 -28
  82. package/.claude/worktrees/emails-list/src/commands/emails/receiving/list.ts +0 -59
  83. package/.claude/worktrees/emails-list/src/commands/emails/receiving/utils.ts +0 -38
  84. package/.claude/worktrees/emails-list/src/commands/emails/send.ts +0 -189
  85. package/.claude/worktrees/emails-list/src/commands/open.ts +0 -24
  86. package/.claude/worktrees/emails-list/src/commands/segments/create.ts +0 -50
  87. package/.claude/worktrees/emails-list/src/commands/segments/delete.ts +0 -47
  88. package/.claude/worktrees/emails-list/src/commands/segments/get.ts +0 -38
  89. package/.claude/worktrees/emails-list/src/commands/segments/index.ts +0 -36
  90. package/.claude/worktrees/emails-list/src/commands/segments/list.ts +0 -58
  91. package/.claude/worktrees/emails-list/src/commands/segments/utils.ts +0 -7
  92. package/.claude/worktrees/emails-list/src/commands/teams/index.ts +0 -10
  93. package/.claude/worktrees/emails-list/src/commands/teams/list.ts +0 -35
  94. package/.claude/worktrees/emails-list/src/commands/teams/remove.ts +0 -83
  95. package/.claude/worktrees/emails-list/src/commands/teams/switch.ts +0 -73
  96. package/.claude/worktrees/emails-list/src/commands/topics/create.ts +0 -73
  97. package/.claude/worktrees/emails-list/src/commands/topics/delete.ts +0 -47
  98. package/.claude/worktrees/emails-list/src/commands/topics/get.ts +0 -42
  99. package/.claude/worktrees/emails-list/src/commands/topics/index.ts +0 -42
  100. package/.claude/worktrees/emails-list/src/commands/topics/list.ts +0 -34
  101. package/.claude/worktrees/emails-list/src/commands/topics/update.ts +0 -59
  102. package/.claude/worktrees/emails-list/src/commands/topics/utils.ts +0 -16
  103. package/.claude/worktrees/emails-list/src/commands/webhooks/create.ts +0 -128
  104. package/.claude/worktrees/emails-list/src/commands/webhooks/delete.ts +0 -49
  105. package/.claude/worktrees/emails-list/src/commands/webhooks/get.ts +0 -42
  106. package/.claude/worktrees/emails-list/src/commands/webhooks/index.ts +0 -44
  107. package/.claude/worktrees/emails-list/src/commands/webhooks/list.ts +0 -55
  108. package/.claude/worktrees/emails-list/src/commands/webhooks/update.ts +0 -83
  109. package/.claude/worktrees/emails-list/src/commands/webhooks/utils.ts +0 -36
  110. package/.claude/worktrees/emails-list/src/commands/whoami.ts +0 -71
  111. package/.claude/worktrees/emails-list/src/lib/actions.ts +0 -157
  112. package/.claude/worktrees/emails-list/src/lib/client.ts +0 -34
  113. package/.claude/worktrees/emails-list/src/lib/config.ts +0 -211
  114. package/.claude/worktrees/emails-list/src/lib/files.ts +0 -15
  115. package/.claude/worktrees/emails-list/src/lib/help-text.ts +0 -38
  116. package/.claude/worktrees/emails-list/src/lib/output.ts +0 -54
  117. package/.claude/worktrees/emails-list/src/lib/pagination.ts +0 -36
  118. package/.claude/worktrees/emails-list/src/lib/prompts.ts +0 -149
  119. package/.claude/worktrees/emails-list/src/lib/spinner.ts +0 -93
  120. package/.claude/worktrees/emails-list/src/lib/table.ts +0 -57
  121. package/.claude/worktrees/emails-list/src/lib/tty.ts +0 -28
  122. package/.claude/worktrees/emails-list/src/lib/version.ts +0 -4
  123. package/.claude/worktrees/emails-list/tests/commands/api-keys/create.test.ts +0 -195
  124. package/.claude/worktrees/emails-list/tests/commands/api-keys/delete.test.ts +0 -156
  125. package/.claude/worktrees/emails-list/tests/commands/api-keys/list.test.ts +0 -133
  126. package/.claude/worktrees/emails-list/tests/commands/auth/login.test.ts +0 -119
  127. package/.claude/worktrees/emails-list/tests/commands/auth/logout.test.ts +0 -146
  128. package/.claude/worktrees/emails-list/tests/commands/broadcasts/create.test.ts +0 -447
  129. package/.claude/worktrees/emails-list/tests/commands/broadcasts/delete.test.ts +0 -182
  130. package/.claude/worktrees/emails-list/tests/commands/broadcasts/get.test.ts +0 -146
  131. package/.claude/worktrees/emails-list/tests/commands/broadcasts/list.test.ts +0 -196
  132. package/.claude/worktrees/emails-list/tests/commands/broadcasts/send.test.ts +0 -161
  133. package/.claude/worktrees/emails-list/tests/commands/broadcasts/update.test.ts +0 -283
  134. package/.claude/worktrees/emails-list/tests/commands/contact-properties/create.test.ts +0 -250
  135. package/.claude/worktrees/emails-list/tests/commands/contact-properties/delete.test.ts +0 -183
  136. package/.claude/worktrees/emails-list/tests/commands/contact-properties/get.test.ts +0 -144
  137. package/.claude/worktrees/emails-list/tests/commands/contact-properties/list.test.ts +0 -180
  138. package/.claude/worktrees/emails-list/tests/commands/contact-properties/update.test.ts +0 -216
  139. package/.claude/worktrees/emails-list/tests/commands/contacts/add-segment.test.ts +0 -188
  140. package/.claude/worktrees/emails-list/tests/commands/contacts/create.test.ts +0 -270
  141. package/.claude/worktrees/emails-list/tests/commands/contacts/delete.test.ts +0 -192
  142. package/.claude/worktrees/emails-list/tests/commands/contacts/get.test.ts +0 -148
  143. package/.claude/worktrees/emails-list/tests/commands/contacts/list.test.ts +0 -175
  144. package/.claude/worktrees/emails-list/tests/commands/contacts/remove-segment.test.ts +0 -166
  145. package/.claude/worktrees/emails-list/tests/commands/contacts/segments.test.ts +0 -167
  146. package/.claude/worktrees/emails-list/tests/commands/contacts/topics.test.ts +0 -163
  147. package/.claude/worktrees/emails-list/tests/commands/contacts/update-topics.test.ts +0 -247
  148. package/.claude/worktrees/emails-list/tests/commands/contacts/update.test.ts +0 -205
  149. package/.claude/worktrees/emails-list/tests/commands/doctor.test.ts +0 -165
  150. package/.claude/worktrees/emails-list/tests/commands/domains/create.test.ts +0 -192
  151. package/.claude/worktrees/emails-list/tests/commands/domains/delete.test.ts +0 -156
  152. package/.claude/worktrees/emails-list/tests/commands/domains/get.test.ts +0 -137
  153. package/.claude/worktrees/emails-list/tests/commands/domains/list.test.ts +0 -164
  154. package/.claude/worktrees/emails-list/tests/commands/domains/update.test.ts +0 -223
  155. package/.claude/worktrees/emails-list/tests/commands/domains/verify.test.ts +0 -117
  156. package/.claude/worktrees/emails-list/tests/commands/emails/batch.test.ts +0 -313
  157. package/.claude/worktrees/emails-list/tests/commands/emails/list.test.ts +0 -196
  158. package/.claude/worktrees/emails-list/tests/commands/emails/receiving/attachment.test.ts +0 -140
  159. package/.claude/worktrees/emails-list/tests/commands/emails/receiving/attachments.test.ts +0 -168
  160. package/.claude/worktrees/emails-list/tests/commands/emails/receiving/get.test.ts +0 -140
  161. package/.claude/worktrees/emails-list/tests/commands/emails/receiving/list.test.ts +0 -181
  162. package/.claude/worktrees/emails-list/tests/commands/emails/send.test.ts +0 -309
  163. package/.claude/worktrees/emails-list/tests/commands/segments/create.test.ts +0 -163
  164. package/.claude/worktrees/emails-list/tests/commands/segments/delete.test.ts +0 -182
  165. package/.claude/worktrees/emails-list/tests/commands/segments/get.test.ts +0 -137
  166. package/.claude/worktrees/emails-list/tests/commands/segments/list.test.ts +0 -173
  167. package/.claude/worktrees/emails-list/tests/commands/teams/list.test.ts +0 -63
  168. package/.claude/worktrees/emails-list/tests/commands/teams/remove.test.ts +0 -103
  169. package/.claude/worktrees/emails-list/tests/commands/teams/switch.test.ts +0 -96
  170. package/.claude/worktrees/emails-list/tests/commands/topics/create.test.ts +0 -191
  171. package/.claude/worktrees/emails-list/tests/commands/topics/delete.test.ts +0 -156
  172. package/.claude/worktrees/emails-list/tests/commands/topics/get.test.ts +0 -125
  173. package/.claude/worktrees/emails-list/tests/commands/topics/list.test.ts +0 -124
  174. package/.claude/worktrees/emails-list/tests/commands/topics/update.test.ts +0 -177
  175. package/.claude/worktrees/emails-list/tests/commands/webhooks/create.test.ts +0 -224
  176. package/.claude/worktrees/emails-list/tests/commands/webhooks/delete.test.ts +0 -156
  177. package/.claude/worktrees/emails-list/tests/commands/webhooks/get.test.ts +0 -125
  178. package/.claude/worktrees/emails-list/tests/commands/webhooks/list.test.ts +0 -177
  179. package/.claude/worktrees/emails-list/tests/commands/webhooks/update.test.ts +0 -206
  180. package/.claude/worktrees/emails-list/tests/commands/whoami.test.ts +0 -99
  181. package/.claude/worktrees/emails-list/tests/helpers.ts +0 -93
  182. package/.claude/worktrees/emails-list/tests/lib/client.test.ts +0 -71
  183. package/.claude/worktrees/emails-list/tests/lib/config.test.ts +0 -414
  184. package/.claude/worktrees/emails-list/tests/lib/files.test.ts +0 -65
  185. package/.claude/worktrees/emails-list/tests/lib/help-text.test.ts +0 -97
  186. package/.claude/worktrees/emails-list/tests/lib/output.test.ts +0 -127
  187. package/.claude/worktrees/emails-list/tests/lib/prompts.test.ts +0 -178
  188. package/.claude/worktrees/emails-list/tests/lib/spinner.test.ts +0 -146
  189. package/.claude/worktrees/emails-list/tests/lib/table.test.ts +0 -63
  190. package/.claude/worktrees/emails-list/tests/lib/tty.test.ts +0 -85
  191. package/.claude/worktrees/emails-list/tsconfig.json +0 -14
package/biome.json CHANGED
@@ -31,6 +31,6 @@
31
31
  }
32
32
  },
33
33
  "files": {
34
- "includes": ["**", "!**/dist", "!**/bun.lock"]
34
+ "includes": ["**", "!**/dist", "!**/bun.lock", "!.claude"]
35
35
  }
36
36
  }
package/bun.lock CHANGED
@@ -9,7 +9,6 @@
9
9
  "@commander-js/extra-typings": "14.0.0",
10
10
  "commander": "14.0.3",
11
11
  "resend": "6.9.3",
12
- "unicode-animations": "1.0.3",
13
12
  },
14
13
  "devDependencies": {
15
14
  "@biomejs/biome": "2.4.6",
@@ -69,8 +68,6 @@
69
68
 
70
69
  "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
71
70
 
72
- "unicode-animations": ["unicode-animations@1.0.3", "", { "dependencies": { "unicode-animations": "^1.0.1" }, "bin": { "unicode-animations": "scripts/demo.cjs" } }, "sha512-+klB2oWwcYZjYWhwP4Pr8UZffWDFVx6jKeIahE6z0QYyM2dwDeDPyn5nevCYbyotxvtT9lh21cVURO1RX0+YMg=="],
73
-
74
71
  "uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="],
75
72
  }
76
73
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "resend-cli",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "The official CLI for Resend",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -32,8 +32,7 @@
32
32
  "commander": "14.0.3",
33
33
  "@commander-js/extra-typings": "14.0.0",
34
34
  "resend": "6.9.3",
35
- "@clack/prompts": "1.1.0",
36
- "unicode-animations": "1.0.3"
35
+ "@clack/prompts": "1.1.0"
37
36
  },
38
37
  "devDependencies": {
39
38
  "@biomejs/biome": "2.4.6",
package/src/cli.ts CHANGED
@@ -15,6 +15,8 @@ import { teamsCommand } from './commands/teams/index';
15
15
  import { topicsCommand } from './commands/topics/index';
16
16
  import { webhooksCommand } from './commands/webhooks/index';
17
17
  import { whoamiCommand } from './commands/whoami';
18
+ import { errorMessage, outputError } from './lib/output';
19
+ import { checkForUpdates } from './lib/update-check';
18
20
  import { PACKAGE_NAME, VERSION } from './lib/version';
19
21
 
20
22
  const BANNER = `
@@ -79,4 +81,12 @@ Examples:
79
81
  .addCommand(openCommand)
80
82
  .addCommand(whoamiCommand);
81
83
 
82
- program.parse();
84
+ program
85
+ .parseAsync()
86
+ .then(() => checkForUpdates().catch(() => {}))
87
+ .catch((err) => {
88
+ outputError({
89
+ message: errorMessage(err, 'An unexpected error occurred'),
90
+ code: 'unexpected_error',
91
+ });
92
+ });
@@ -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,11 +133,7 @@ export const loginCommand = new Command('login')
127
133
  );
128
134
  }
129
135
 
130
- const spinner = createSpinner(
131
- 'Validating API key...',
132
- 'braille',
133
- globalOpts.quiet,
134
- );
136
+ const spinner = createSpinner('Validating API key...', globalOpts.quiet);
135
137
 
136
138
  try {
137
139
  const resend = new Resend(apiKey);
@@ -150,6 +152,17 @@ export const loginCommand = new Command('login')
150
152
 
151
153
  let teamName = globalOpts.team;
152
154
 
155
+ if (teamName) {
156
+ const teamError = validateTeamName(teamName);
157
+ if (teamError) {
158
+ outputError(
159
+ { message: teamError, code: 'invalid_team_name' },
160
+ { json: globalOpts.json },
161
+ );
162
+ return;
163
+ }
164
+ }
165
+
153
166
  if (!teamName && isInteractive()) {
154
167
  const existingTeams = listTeams();
155
168
  if (existingTeams.length > 0) {
@@ -173,8 +186,7 @@ export const loginCommand = new Command('login')
173
186
  if (choice === '__new__') {
174
187
  const newName = await p.text({
175
188
  message: 'Enter a name for the new team:',
176
- validate: (v) =>
177
- !v || v.length === 0 ? 'Team name is required' : undefined,
189
+ validate: (v) => validateTeamName(v as string),
178
190
  });
179
191
  if (p.isCancel(newName)) {
180
192
  cancelAndExit('Login cancelled.');
@@ -191,6 +203,21 @@ export const loginCommand = new Command('login')
191
203
  const configPath = storeApiKey(apiKey, teamName);
192
204
  const teamLabel = teamName || 'default';
193
205
 
206
+ // Auto-switch to the newly added team
207
+ if (teamName) {
208
+ try {
209
+ setActiveTeam(teamName);
210
+ } catch (err) {
211
+ outputError(
212
+ {
213
+ message: errorMessage(err, 'Failed to switch team'),
214
+ code: 'switch_failed',
215
+ },
216
+ { json: globalOpts.json },
217
+ );
218
+ }
219
+ }
220
+
194
221
  if (globalOpts.json) {
195
222
  outputResult(
196
223
  { success: true, config_path: configPath, team: teamLabel },
@@ -1,6 +1,3 @@
1
- import { existsSync } from 'node:fs';
2
- import { homedir } from 'node:os';
3
- import { join } from 'node:path';
4
1
  import { Command } from '@commander-js/extra-typings';
5
2
  import { Resend } from 'resend';
6
3
  import type { GlobalOpts } from '../lib/client';
@@ -9,7 +6,8 @@ import { buildHelpText } from '../lib/help-text';
9
6
  import { errorMessage, outputResult } from '../lib/output';
10
7
  import { createSpinner } from '../lib/spinner';
11
8
  import { isInteractive } from '../lib/tty';
12
- import { PACKAGE_NAME, VERSION } from '../lib/version';
9
+ import { GITHUB_RELEASES_URL } from '../lib/update-check';
10
+ import { VERSION } from '../lib/version';
13
11
 
14
12
  type CheckStatus = 'pass' | 'warn' | 'fail';
15
13
 
@@ -20,21 +18,12 @@ type CheckResult = {
20
18
  detail?: string;
21
19
  };
22
20
 
23
- const statusIcons: Record<CheckStatus, string> = {
24
- pass: '\x1B[32m✔\x1B[0m',
25
- warn: '\x1B[33m!\x1B[0m',
26
- fail: '\x1B[31m✗\x1B[0m',
27
- };
28
-
29
21
  async function checkCliVersion(): Promise<CheckResult> {
30
22
  try {
31
- const encodedName = encodeURIComponent(PACKAGE_NAME);
32
- const res = await fetch(
33
- `https://registry.npmjs.org/${encodedName}/latest`,
34
- {
35
- signal: AbortSignal.timeout(5000),
36
- },
37
- );
23
+ const res = await fetch(GITHUB_RELEASES_URL, {
24
+ headers: { Accept: 'application/vnd.github.v3+json' },
25
+ signal: AbortSignal.timeout(5000),
26
+ });
38
27
  if (!res.ok) {
39
28
  return {
40
29
  name: 'CLI Version',
@@ -42,8 +31,19 @@ async function checkCliVersion(): Promise<CheckResult> {
42
31
  message: `v${VERSION} (could not check for updates)`,
43
32
  };
44
33
  }
45
- const data = (await res.json()) as { version?: string };
46
- const latest = data.version ?? 'unknown';
34
+ const data = (await res.json()) as {
35
+ tag_name?: string;
36
+ prerelease?: boolean;
37
+ draft?: boolean;
38
+ };
39
+ if (data.prerelease || data.draft) {
40
+ return {
41
+ name: 'CLI Version',
42
+ status: 'warn',
43
+ message: `v${VERSION} (could not check for updates)`,
44
+ };
45
+ }
46
+ const latest = data.tag_name?.replace(/^v/, '') ?? 'unknown';
47
47
  if (latest === VERSION) {
48
48
  return {
49
49
  name: 'CLI Version',
@@ -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',
@@ -84,8 +84,10 @@ function checkApiKeyPresence(): CheckResult {
84
84
  };
85
85
  }
86
86
 
87
- async function checkApiValidationAndDomains(): Promise<CheckResult> {
88
- const resolved = resolveApiKey();
87
+ async function checkApiValidationAndDomains(
88
+ flagValue?: string,
89
+ ): Promise<CheckResult> {
90
+ const resolved = resolveApiKey(flagValue);
89
91
  if (!resolved) {
90
92
  return {
91
93
  name: 'API Validation',
@@ -143,73 +145,8 @@ async function checkApiValidationAndDomains(): Promise<CheckResult> {
143
145
  }
144
146
  }
145
147
 
146
- function checkAgentDetection(): CheckResult {
147
- const home = homedir();
148
- const agents: { name: string; found: boolean }[] = [];
149
-
150
- // OpenClaw
151
- agents.push({
152
- name: 'OpenClaw',
153
- found: existsSync(join(home, 'clawd', 'skills')),
154
- });
155
-
156
- // Cursor
157
- agents.push({ name: 'Cursor', found: existsSync(join(home, '.cursor')) });
158
-
159
- // Claude Desktop
160
- const claudeConfigPaths =
161
- process.platform === 'darwin'
162
- ? [
163
- join(
164
- home,
165
- 'Library',
166
- 'Application Support',
167
- 'Claude',
168
- 'claude_desktop_config.json',
169
- ),
170
- ]
171
- : process.platform === 'win32'
172
- ? [
173
- join(
174
- process.env.APPDATA ?? '',
175
- 'Claude',
176
- 'claude_desktop_config.json',
177
- ),
178
- ]
179
- : [join(home, '.config', 'Claude', 'claude_desktop_config.json')];
180
- agents.push({
181
- name: 'Claude Desktop',
182
- found: claudeConfigPaths.some(existsSync),
183
- });
184
-
185
- // VS Code MCP
186
- agents.push({
187
- name: 'VS Code',
188
- found: existsSync(join(process.cwd(), '.vscode', 'mcp.json')),
189
- });
190
-
191
- const detected = agents.filter((a) => a.found);
192
-
193
- if (detected.length === 0) {
194
- return {
195
- name: 'AI Agents',
196
- status: 'pass',
197
- message: 'No AI agents detected',
198
- };
199
- }
200
-
201
- return {
202
- name: 'AI Agents',
203
- status: 'pass',
204
- message: `Detected: ${detected.map((a) => a.name).join(', ')}`,
205
- detail: 'Future: run `resend setup <agent>` to configure integration',
206
- };
207
- }
208
-
209
148
  export const doctorCommand = new Command('doctor')
210
- .description(
211
- 'Check CLI version, API key, domain status, and AI agent detection',
212
- )
149
+ .description('Check CLI version, API key, and domain status')
213
150
  .addHelpText(
214
151
  'after',
215
152
  buildHelpText({
@@ -217,9 +154,8 @@ export const doctorCommand = new Command('doctor')
217
154
  context: `Checks performed:
218
155
  CLI Version Is the installed version up to date?
219
156
  API Key Is a key present (--api-key, RESEND_API_KEY, or credentials file)?
220
- API Validation Is the key valid and accepted by the Resend API?
221
- AI Agents Detected: Claude Desktop, Cursor, VS Code MCP, OpenClaw`,
222
- output: ` {\n "ok": true,\n "checks": [\n {"name":"CLI Version","status":"pass","message":"v0.1.0 (latest)"},\n {"name":"API Key","status":"pass","message":"re_...abcd (source: env)"},\n {"name":"Domains","status":"pass","message":"1 verified, 0 pending"},\n {"name":"AI Agents","status":"pass","message":"Detected: Claude Desktop"}\n ]\n }\n status values: "pass" | "warn" | "fail"\n Exit code 1 if any check has status "fail"`,
157
+ API Validation Is the key valid and accepted by the Resend API?`,
158
+ output: ` {\n "ok": true,\n "checks": [\n {"name":"CLI Version","status":"pass","message":"v0.1.0 (latest)"},\n {"name":"API Key","status":"pass","message":"re_...abcd (source: env)"},\n {"name":"Domains","status":"pass","message":"1 verified, 0 pending"}\n ]\n }\n status values: "pass" | "warn" | "fail"\n Exit code 1 if any check has status "fail"`,
223
159
  examples: ['resend doctor', 'resend doctor --json'],
224
160
  }),
225
161
  )
@@ -233,9 +169,7 @@ export const doctorCommand = new Command('doctor')
233
169
  }
234
170
 
235
171
  // Check 1: CLI Version
236
- let spinner = interactive
237
- ? createSpinner('Checking CLI version...', 'orbit')
238
- : null;
172
+ let spinner = interactive ? createSpinner('Checking CLI version...') : null;
239
173
  const versionCheck = await checkCliVersion();
240
174
  checks.push(versionCheck);
241
175
  if (versionCheck.status === 'warn') {
@@ -245,8 +179,8 @@ export const doctorCommand = new Command('doctor')
245
179
  }
246
180
 
247
181
  // Check 2: API Key
248
- spinner = interactive ? createSpinner('Checking API key...', 'scan') : null;
249
- const keyCheck = checkApiKeyPresence();
182
+ spinner = interactive ? createSpinner('Checking API key...') : null;
183
+ const keyCheck = checkApiKeyPresence(globalOpts.apiKey);
250
184
  checks.push(keyCheck);
251
185
  if (keyCheck.status === 'fail') {
252
186
  spinner?.fail(keyCheck.message);
@@ -256,9 +190,9 @@ export const doctorCommand = new Command('doctor')
256
190
 
257
191
  // Check 3: API Validation + Domains
258
192
  spinner = interactive
259
- ? createSpinner('Validating API key & domains...', 'scan')
193
+ ? createSpinner('Validating API key & domains...')
260
194
  : null;
261
- const domainCheck = await checkApiValidationAndDomains();
195
+ const domainCheck = await checkApiValidationAndDomains(globalOpts.apiKey);
262
196
  checks.push(domainCheck);
263
197
  if (domainCheck.status === 'fail') {
264
198
  spinner?.fail(domainCheck.message);
@@ -268,26 +202,10 @@ export const doctorCommand = new Command('doctor')
268
202
  spinner?.stop(domainCheck.message);
269
203
  }
270
204
 
271
- // Check 4: Agent Detection
272
- spinner = interactive
273
- ? createSpinner('Detecting AI agents...', 'scan')
274
- : null;
275
- const agentCheck = checkAgentDetection();
276
- checks.push(agentCheck);
277
- spinner?.stop(agentCheck.message);
278
-
279
205
  const hasFails = checks.some((c) => c.status === 'fail');
280
206
 
281
207
  if (!globalOpts.json && isInteractive()) {
282
208
  console.log('');
283
- for (const check of checks) {
284
- const icon = statusIcons[check.status];
285
- console.log(` ${icon} ${check.name}: ${check.message}`);
286
- if (check.detail) {
287
- console.log(` ${check.detail}`);
288
- }
289
- }
290
- console.log('');
291
209
  } else {
292
210
  outputResult({ ok: !hasFails, checks }, { json: globalOpts.json });
293
211
  }
@@ -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/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,36 +196,22 @@ export function listTeams(): Array<{ name: string; active: boolean }> {
176
196
  }));
177
197
  }
178
198
 
179
- export function maskKey(key: string): string {
180
- if (key.length <= 7) {
181
- return `${key.slice(0, 3)}...`;
199
+ export function validateTeamName(name: string): string | undefined {
200
+ if (!name || name.length === 0) {
201
+ return 'Team name must not be empty';
182
202
  }
183
- return `${key.slice(0, 3)}...${key.slice(-4)}`;
184
- }
185
-
186
- export function removeTeam(teamName: string): void {
187
- const creds = readCredentials();
188
- if (!creds) {
189
- throw new Error('No credentials file found.');
203
+ if (name.length > 64) {
204
+ return 'Team name must be 64 characters or fewer';
190
205
  }
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';
206
+ if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
207
+ return 'Team name must contain only letters, numbers, dashes, and underscores';
202
208
  }
209
+ return undefined;
210
+ }
203
211
 
204
- if (Object.keys(creds.teams).length === 0) {
205
- const { unlinkSync } = require('node:fs');
206
- unlinkSync(getCredentialsPath());
207
- return;
212
+ export function maskKey(key: string): string {
213
+ if (key.length <= 7) {
214
+ return `${key.slice(0, 3)}...`;
208
215
  }
209
-
210
- writeCredentials(creds);
216
+ return `${key.slice(0, 3)}...${key.slice(-4)}`;
211
217
  }
@@ -1,4 +1,3 @@
1
- import spinners from 'unicode-animations';
2
1
  import type { GlobalOpts } from './client';
3
2
  import { errorMessage, outputError } from './output';
4
3
  import { isInteractive, isUnicodeSupported } from './tty';
@@ -9,6 +8,19 @@ const TICK = isUnicodeSupported ? String.fromCodePoint(0x2714) : 'v'; // ✔
9
8
  const WARN = isUnicodeSupported ? String.fromCodePoint(0x26a0) : '!'; // ⚠
10
9
  const CROSS = isUnicodeSupported ? String.fromCodePoint(0x2717) : 'x'; // ✗
11
10
 
11
+ // Braille spinner: cycle through U+2800-block dot patterns.
12
+ const SPINNER_FRAMES = [
13
+ '\u2839',
14
+ '\u2838',
15
+ '\u2834',
16
+ '\u2826',
17
+ '\u2807',
18
+ '\u280F',
19
+ '\u2819',
20
+ '\u2839',
21
+ ];
22
+ const SPINNER_INTERVAL = 80;
23
+
12
24
  type SdkResponse<T> = { data: T | null; error: { message: string } | null };
13
25
 
14
26
  /**
@@ -21,7 +33,7 @@ export async function withSpinner<T>(
21
33
  errorCode: string,
22
34
  globalOpts: GlobalOpts,
23
35
  ): Promise<T> {
24
- const spinner = createSpinner(messages.loading, 'braille', globalOpts.quiet);
36
+ const spinner = createSpinner(messages.loading, globalOpts.quiet);
25
37
  try {
26
38
  const { data, error } = await call();
27
39
  if (error) {
@@ -49,13 +61,7 @@ export async function withSpinner<T>(
49
61
  }
50
62
  }
51
63
 
52
- export type SpinnerName = keyof typeof spinners;
53
-
54
- export function createSpinner(
55
- message: string,
56
- name: SpinnerName = 'braille',
57
- quiet?: boolean,
58
- ) {
64
+ export function createSpinner(message: string, quiet?: boolean) {
59
65
  if (quiet || !isInteractive()) {
60
66
  return {
61
67
  update(_msg: string) {},
@@ -65,7 +71,8 @@ export function createSpinner(
65
71
  };
66
72
  }
67
73
 
68
- const { frames, interval } = spinners[name];
74
+ const frames = isUnicodeSupported ? SPINNER_FRAMES : ['-', '\\', '|', '/'];
75
+ const interval = SPINNER_INTERVAL;
69
76
  let i = 0;
70
77
  let text = message;
71
78