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
@@ -1,39 +0,0 @@
1
- import { Command } from '@commander-js/extra-typings';
2
- import { runList } from '../../lib/actions';
3
- import type { GlobalOpts } from '../../lib/client';
4
- import { buildHelpText } from '../../lib/help-text';
5
- import { renderSegmentsTable } from '../segments/utils';
6
- import { segmentContactIdentifier } from './utils';
7
-
8
- export const listContactSegmentsCommand = new Command('segments')
9
- .description('List the segments a contact belongs to')
10
- .argument('<id>', 'Contact UUID or email address')
11
- .addHelpText(
12
- 'after',
13
- buildHelpText({
14
- context: `The <id> argument accepts either a UUID or an email address.`,
15
- output: ` {"object":"list","data":[{"id":"<segment-uuid>","name":"Newsletter Subscribers","created_at":"..."}],"has_more":false}`,
16
- errorCodes: ['auth_error', 'list_error'],
17
- examples: [
18
- 'resend contacts segments 479e3145-dd38-4932-8c0c-e58b548c9e76',
19
- 'resend contacts segments user@example.com',
20
- 'resend contacts segments 479e3145-dd38-4932-8c0c-e58b548c9e76 --json',
21
- ],
22
- }),
23
- )
24
- .action(async (id, _opts, cmd) => {
25
- const globalOpts = cmd.optsWithGlobals() as GlobalOpts;
26
- await runList(
27
- {
28
- spinner: {
29
- loading: 'Fetching segments...',
30
- success: 'Segments fetched',
31
- fail: 'Failed to list segments',
32
- },
33
- sdkCall: (resend) =>
34
- resend.contacts.segments.list(segmentContactIdentifier(id)),
35
- onInteractive: (list) => console.log(renderSegmentsTable(list.data)),
36
- },
37
- globalOpts,
38
- );
39
- });
@@ -1,45 +0,0 @@
1
- import { Command } from '@commander-js/extra-typings';
2
- import { runList } from '../../lib/actions';
3
- import type { GlobalOpts } from '../../lib/client';
4
- import { buildHelpText } from '../../lib/help-text';
5
- import { contactIdentifier, renderContactTopicsTable } from './utils';
6
-
7
- export const listContactTopicsCommand = new Command('topics')
8
- .description("List a contact's topic subscriptions")
9
- .argument('<id>', 'Contact UUID or email address')
10
- .addHelpText(
11
- 'after',
12
- buildHelpText({
13
- context: `The <id> argument accepts either a UUID or an email address.
14
-
15
- Topics control which broadcast email types a contact receives.
16
- subscription values: "opt_in" (receiving) | "opt_out" (not receiving)
17
-
18
- Use "resend contacts update-topics <id>" to change subscription statuses.`,
19
- output: ` {"object":"list","data":[{"id":"...","name":"Product Updates","description":"...","subscription":"opt_in"}],"has_more":false}`,
20
- errorCodes: ['auth_error', 'list_error'],
21
- examples: [
22
- 'resend contacts topics 479e3145-dd38-4932-8c0c-e58b548c9e76',
23
- 'resend contacts topics user@example.com',
24
- 'resend contacts topics 479e3145-dd38-4932-8c0c-e58b548c9e76 --json',
25
- ],
26
- }),
27
- )
28
- .action(async (id, _opts, cmd) => {
29
- const globalOpts = cmd.optsWithGlobals() as GlobalOpts;
30
- // ListContactTopicsBaseOptions uses optional { id?, email? } (not a discriminated
31
- // union), so contactIdentifier's result is directly assignable without a cast.
32
- await runList(
33
- {
34
- spinner: {
35
- loading: 'Fetching topic subscriptions...',
36
- success: 'Topic subscriptions fetched',
37
- fail: 'Failed to list topic subscriptions',
38
- },
39
- sdkCall: (resend) => resend.contacts.topics.list(contactIdentifier(id)),
40
- onInteractive: (list) =>
41
- console.log(renderContactTopicsTable(list.data)),
42
- },
43
- globalOpts,
44
- );
45
- });
@@ -1,90 +0,0 @@
1
- import * as p from '@clack/prompts';
2
- import { Command } from '@commander-js/extra-typings';
3
- import type { GlobalOpts } from '../../lib/client';
4
- import { requireClient } from '../../lib/client';
5
- import { buildHelpText } from '../../lib/help-text';
6
- import { outputError, outputResult } from '../../lib/output';
7
- import { cancelAndExit } from '../../lib/prompts';
8
- import { withSpinner } from '../../lib/spinner';
9
- import { isInteractive } from '../../lib/tty';
10
- import { contactIdentifier, parseTopicsJson } from './utils';
11
-
12
- export const updateContactTopicsCommand = new Command('update-topics')
13
- .description("Update a contact's topic subscription statuses")
14
- .argument('<id>', 'Contact UUID or email address')
15
- .option(
16
- '--topics <json>',
17
- 'JSON array of topic subscriptions (required) — e.g. \'[{"id":"topic-uuid","subscription":"opt_in"}]\'',
18
- )
19
- .addHelpText(
20
- 'after',
21
- buildHelpText({
22
- context: `The <id> argument accepts either a UUID or an email address.
23
-
24
- Non-interactive: --topics is required.
25
-
26
- Topics JSON format:
27
- '[{"id":"<topic-uuid>","subscription":"opt_in"}]'
28
- subscription values: "opt_in" | "opt_out"
29
-
30
- This operation replaces all topic subscriptions for the specified topics.
31
- Topics not included in the array are left unchanged.`,
32
- output: ` {"id":"<contact-id>"}`,
33
- errorCodes: [
34
- 'auth_error',
35
- 'missing_topics',
36
- 'invalid_topics',
37
- 'update_topics_error',
38
- ],
39
- examples: [
40
- `resend contacts update-topics 479e3145-dd38-4932-8c0c-e58b548c9e76 --topics '[{"id":"topic-uuid","subscription":"opt_in"}]'`,
41
- `resend contacts update-topics user@example.com --topics '[{"id":"t1","subscription":"opt_out"},{"id":"t2","subscription":"opt_in"}]' --json`,
42
- ],
43
- }),
44
- )
45
- .action(async (id, opts, cmd) => {
46
- const globalOpts = cmd.optsWithGlobals() as GlobalOpts;
47
- const resend = requireClient(globalOpts);
48
-
49
- let topicsJson = opts.topics;
50
-
51
- if (!topicsJson) {
52
- if (!isInteractive()) {
53
- outputError(
54
- { message: 'Missing --topics flag.', code: 'missing_topics' },
55
- { json: globalOpts.json },
56
- );
57
- }
58
- const result = await p.text({
59
- message:
60
- 'Topics JSON (e.g. \'[{"id":"topic-uuid","subscription":"opt_in"}]\')',
61
- placeholder: '[{"id":"topic-uuid","subscription":"opt_in"}]',
62
- validate: (v) => (!v ? 'Required' : undefined),
63
- });
64
- if (p.isCancel(result)) {
65
- cancelAndExit('Cancelled.');
66
- }
67
- topicsJson = result;
68
- }
69
-
70
- const topics = parseTopicsJson(topicsJson, globalOpts);
71
-
72
- // contactIdentifier's result is directly assignable: UpdateContactTopicsBaseOptions
73
- // uses optional { id?, email? } (not a discriminated union).
74
- const data = await withSpinner(
75
- {
76
- loading: 'Updating topic subscriptions...',
77
- success: 'Topic subscriptions updated',
78
- fail: 'Failed to update topic subscriptions',
79
- },
80
- () => resend.contacts.topics.update({ ...contactIdentifier(id), topics }),
81
- 'update_topics_error',
82
- globalOpts,
83
- );
84
-
85
- if (!globalOpts.json && isInteractive()) {
86
- console.log(`Topic subscriptions updated for contact: ${id}`);
87
- } else {
88
- outputResult(data, { json: globalOpts.json });
89
- }
90
- });
@@ -1,77 +0,0 @@
1
- import { Command } from '@commander-js/extra-typings';
2
- import type { UpdateContactOptions } from 'resend';
3
- import { runWrite } from '../../lib/actions';
4
- import type { GlobalOpts } from '../../lib/client';
5
- import { buildHelpText } from '../../lib/help-text';
6
- import { contactIdentifier, parsePropertiesJson } from './utils';
7
-
8
- export const updateContactCommand = new Command('update')
9
- .description("Update a contact's subscription status or custom properties")
10
- .argument(
11
- '<id>',
12
- 'Contact UUID or email address — both are accepted by the API',
13
- )
14
- .option(
15
- '--unsubscribed',
16
- 'Globally unsubscribe the contact from all broadcasts',
17
- )
18
- .option(
19
- '--no-unsubscribed',
20
- 'Re-subscribe the contact (clears the global unsubscribe flag)',
21
- )
22
- .option(
23
- '--properties <json>',
24
- 'JSON object of properties to merge (e.g. \'{"company":"Acme"}\'); set a key to null to clear it',
25
- )
26
- .addHelpText(
27
- 'after',
28
- buildHelpText({
29
- context: `The <id> argument accepts either a UUID or an email address.
30
-
31
- Subscription toggle:
32
- --unsubscribed Sets unsubscribed: true — contact will not receive any broadcasts.
33
- --no-unsubscribed Sets unsubscribed: false — re-enables broadcast delivery.
34
- Omitting both flags leaves the subscription status unchanged.
35
-
36
- Properties: --properties merges the given JSON object with existing properties.
37
- Set a key to null to clear it: '{"company":null}'.`,
38
- output: ` {"object":"contact","id":"<id>"}`,
39
- errorCodes: ['auth_error', 'invalid_properties', 'update_error'],
40
- examples: [
41
- 'resend contacts update 479e3145-dd38-4932-8c0c-e58b548c9e76 --unsubscribed',
42
- 'resend contacts update user@example.com --no-unsubscribed',
43
- `resend contacts update 479e3145-dd38-4932-8c0c-e58b548c9e76 --properties '{"plan":"pro"}'`,
44
- 'resend contacts update user@example.com --unsubscribed --json',
45
- ],
46
- }),
47
- )
48
- .action(async (id, opts, cmd) => {
49
- const globalOpts = cmd.optsWithGlobals() as GlobalOpts;
50
-
51
- const properties = parsePropertiesJson(opts.properties, globalOpts);
52
-
53
- // contactIdentifier resolves UUID vs email. The spread of a discriminated
54
- // union requires an explicit cast because TypeScript cannot narrow it
55
- // through a spread at the call site.
56
- const payload = {
57
- ...contactIdentifier(id),
58
- ...(opts.unsubscribed !== undefined && {
59
- unsubscribed: opts.unsubscribed,
60
- }),
61
- ...(properties && { properties }),
62
- } as UpdateContactOptions;
63
-
64
- await runWrite(
65
- {
66
- spinner: {
67
- loading: 'Updating contact...',
68
- success: 'Contact updated',
69
- fail: 'Failed to update contact',
70
- },
71
- sdkCall: (resend) => resend.contacts.update(payload),
72
- errorCode: 'update_error',
73
- successMsg: `Contact updated: ${id}`,
74
- },
75
- globalOpts,
76
- );
77
- });
@@ -1,119 +0,0 @@
1
- import type { ContactSegmentsBaseOptions, ContactTopic } from 'resend';
2
- import type { GlobalOpts } from '../../lib/client';
3
- import { outputError } from '../../lib/output';
4
- import { renderTable } from '../../lib/table';
5
-
6
- // ─── Table renderers ─────────────────────────────────────────────────────────
7
-
8
- export function renderContactsTable(
9
- contacts: Array<{
10
- id: string;
11
- email: string;
12
- first_name: string | null;
13
- last_name: string | null;
14
- unsubscribed: boolean;
15
- }>,
16
- ): string {
17
- const rows = contacts.map((c) => [
18
- c.email,
19
- c.first_name ?? '',
20
- c.last_name ?? '',
21
- c.unsubscribed ? 'yes' : 'no',
22
- c.id,
23
- ]);
24
- return renderTable(
25
- ['Email', 'First Name', 'Last Name', 'Unsubscribed', 'ID'],
26
- rows,
27
- '(no contacts)',
28
- );
29
- }
30
-
31
- export function renderContactTopicsTable(topics: ContactTopic[]): string {
32
- const rows = topics.map((t) => [
33
- t.name,
34
- t.subscription,
35
- t.id,
36
- t.description ?? '',
37
- ]);
38
- return renderTable(
39
- ['Name', 'Subscription', 'ID', 'Description'],
40
- rows,
41
- '(no topic subscriptions)',
42
- );
43
- }
44
-
45
- // ─── Contact identifier helpers ───────────────────────────────────────────────
46
- //
47
- // The Resend SDK uses two different discriminated-union shapes depending on
48
- // the API surface:
49
- //
50
- // • contactIdentifier — produces { id } | { email } for endpoints that
51
- // accept SelectingField (update, get, remove) or { id?, email? }
52
- // (topics list/update).
53
- //
54
- // • segmentContactIdentifier — produces { contactId } | { email } for the
55
- // contacts.segments.* endpoints, which use ContactSegmentsBaseOptions.
56
- //
57
- // Centralising the `str.includes('@')` check here prevents it from drifting
58
- // across six separate command files.
59
-
60
- export function contactIdentifier(
61
- id: string,
62
- ): { id: string } | { email: string } {
63
- return id.includes('@') ? { email: id } : { id };
64
- }
65
-
66
- export function segmentContactIdentifier(
67
- id: string,
68
- ): ContactSegmentsBaseOptions {
69
- return id.includes('@') ? { email: id } : { contactId: id };
70
- }
71
-
72
- // ─── JSON flag helpers ────────────────────────────────────────────────────────
73
-
74
- export function parseTopicsJson(
75
- raw: string,
76
- globalOpts: GlobalOpts,
77
- ): Array<{ id: string; subscription: 'opt_in' | 'opt_out' }> {
78
- let parsed: unknown;
79
- try {
80
- parsed = JSON.parse(raw);
81
- } catch {
82
- outputError(
83
- {
84
- message:
85
- 'Invalid --topics JSON. Expected an array of {id, subscription} objects.',
86
- code: 'invalid_topics',
87
- },
88
- { json: globalOpts.json },
89
- );
90
- }
91
- if (!Array.isArray(parsed)) {
92
- outputError(
93
- {
94
- message:
95
- 'Invalid --topics JSON. Expected an array of {id, subscription} objects.',
96
- code: 'invalid_topics',
97
- },
98
- { json: globalOpts.json },
99
- );
100
- }
101
- return parsed as Array<{ id: string; subscription: 'opt_in' | 'opt_out' }>;
102
- }
103
-
104
- export function parsePropertiesJson(
105
- raw: string | undefined,
106
- globalOpts: GlobalOpts,
107
- ): Record<string, string | number | null> | undefined {
108
- if (!raw) {
109
- return undefined;
110
- }
111
- try {
112
- return JSON.parse(raw) as Record<string, string | number | null>;
113
- } catch {
114
- outputError(
115
- { message: 'Invalid --properties JSON.', code: 'invalid_properties' },
116
- { json: globalOpts.json },
117
- );
118
- }
119
- }
@@ -1,298 +0,0 @@
1
- import { existsSync } from 'node:fs';
2
- import { homedir } from 'node:os';
3
- import { join } from 'node:path';
4
- import { Command } from '@commander-js/extra-typings';
5
- import { Resend } from 'resend';
6
- import type { GlobalOpts } from '../lib/client';
7
- import { maskKey, resolveApiKey } from '../lib/config';
8
- import { buildHelpText } from '../lib/help-text';
9
- import { errorMessage, outputResult } from '../lib/output';
10
- import { createSpinner } from '../lib/spinner';
11
- import { isInteractive } from '../lib/tty';
12
- import { PACKAGE_NAME, VERSION } from '../lib/version';
13
-
14
- type CheckStatus = 'pass' | 'warn' | 'fail';
15
-
16
- type CheckResult = {
17
- name: string;
18
- status: CheckStatus;
19
- message: string;
20
- detail?: string;
21
- };
22
-
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
- async function checkCliVersion(): Promise<CheckResult> {
30
- 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
- );
38
- if (!res.ok) {
39
- return {
40
- name: 'CLI Version',
41
- status: 'warn',
42
- message: `v${VERSION} (could not check for updates)`,
43
- };
44
- }
45
- const data = (await res.json()) as { version?: string };
46
- const latest = data.version ?? 'unknown';
47
- if (latest === VERSION) {
48
- return {
49
- name: 'CLI Version',
50
- status: 'pass',
51
- message: `v${VERSION} (latest)`,
52
- };
53
- }
54
- return {
55
- name: 'CLI Version',
56
- status: 'warn',
57
- message: `v${VERSION} (latest: v${latest})`,
58
- detail: 'Update available',
59
- };
60
- } catch {
61
- return {
62
- name: 'CLI Version',
63
- status: 'warn',
64
- message: `v${VERSION} (could not check for updates)`,
65
- };
66
- }
67
- }
68
-
69
- function checkApiKeyPresence(): CheckResult {
70
- const resolved = resolveApiKey();
71
- if (!resolved) {
72
- return {
73
- name: 'API Key',
74
- status: 'fail',
75
- message: 'No API key found',
76
- detail: 'Run: resend login',
77
- };
78
- }
79
- const teamInfo = resolved.team ? `, team: ${resolved.team}` : '';
80
- return {
81
- name: 'API Key',
82
- status: 'pass',
83
- message: `${maskKey(resolved.key)} (source: ${resolved.source}${teamInfo})`,
84
- };
85
- }
86
-
87
- async function checkApiValidationAndDomains(): Promise<CheckResult> {
88
- const resolved = resolveApiKey();
89
- if (!resolved) {
90
- return {
91
- name: 'API Validation',
92
- status: 'fail',
93
- message: 'Skipped — no API key',
94
- };
95
- }
96
-
97
- try {
98
- const resend = new Resend(resolved.key);
99
- const { data, error } = await resend.domains.list();
100
-
101
- if (error) {
102
- return {
103
- name: 'API Validation',
104
- status: 'fail',
105
- message: `API key invalid: ${error.message}`,
106
- };
107
- }
108
-
109
- const domains = data?.data ?? [];
110
- const verified = domains.filter((d) => d.status === 'verified');
111
- const pending = domains.filter((d) => d.status !== 'verified');
112
-
113
- if (domains.length === 0) {
114
- return {
115
- name: 'Domains',
116
- status: 'warn',
117
- message: 'No domains configured',
118
- detail: 'Add a domain at https://resend.com/domains',
119
- };
120
- }
121
-
122
- if (verified.length === 0) {
123
- return {
124
- name: 'Domains',
125
- status: 'warn',
126
- message: `${pending.length} domain(s) pending verification`,
127
- detail: domains.map((d) => `${d.name} (${d.status})`).join(', '),
128
- };
129
- }
130
-
131
- return {
132
- name: 'Domains',
133
- status: 'pass',
134
- message: `${verified.length} verified, ${pending.length} pending`,
135
- detail: domains.map((d) => `${d.name} (${d.status})`).join(', '),
136
- };
137
- } catch (err) {
138
- return {
139
- name: 'API Validation',
140
- status: 'fail',
141
- message: errorMessage(err, 'Failed to validate API key'),
142
- };
143
- }
144
- }
145
-
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
- export const doctorCommand = new Command('doctor')
210
- .description(
211
- 'Check CLI version, API key, domain status, and AI agent detection',
212
- )
213
- .addHelpText(
214
- 'after',
215
- buildHelpText({
216
- setup: true,
217
- context: `Checks performed:
218
- CLI Version Is the installed version up to date?
219
- 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"`,
223
- examples: ['resend doctor', 'resend doctor --json'],
224
- }),
225
- )
226
- .action(async (_opts, cmd) => {
227
- const globalOpts = cmd.optsWithGlobals() as GlobalOpts;
228
- const checks: CheckResult[] = [];
229
- const interactive = isInteractive() && !globalOpts.json;
230
-
231
- if (interactive) {
232
- console.log('\n Resend Doctor\n');
233
- }
234
-
235
- // Check 1: CLI Version
236
- let spinner = interactive
237
- ? createSpinner('Checking CLI version...', 'orbit')
238
- : null;
239
- const versionCheck = await checkCliVersion();
240
- checks.push(versionCheck);
241
- if (versionCheck.status === 'warn') {
242
- spinner?.warn(versionCheck.message);
243
- } else {
244
- spinner?.stop(versionCheck.message);
245
- }
246
-
247
- // Check 2: API Key
248
- spinner = interactive ? createSpinner('Checking API key...', 'scan') : null;
249
- const keyCheck = checkApiKeyPresence();
250
- checks.push(keyCheck);
251
- if (keyCheck.status === 'fail') {
252
- spinner?.fail(keyCheck.message);
253
- } else {
254
- spinner?.stop(keyCheck.message);
255
- }
256
-
257
- // Check 3: API Validation + Domains
258
- spinner = interactive
259
- ? createSpinner('Validating API key & domains...', 'scan')
260
- : null;
261
- const domainCheck = await checkApiValidationAndDomains();
262
- checks.push(domainCheck);
263
- if (domainCheck.status === 'fail') {
264
- spinner?.fail(domainCheck.message);
265
- } else if (domainCheck.status === 'warn') {
266
- spinner?.warn(domainCheck.message);
267
- } else {
268
- spinner?.stop(domainCheck.message);
269
- }
270
-
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
- const hasFails = checks.some((c) => c.status === 'fail');
280
-
281
- if (!globalOpts.json && isInteractive()) {
282
- 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
- } else {
292
- outputResult({ ok: !hasFails, checks }, { json: globalOpts.json });
293
- }
294
-
295
- if (hasFails) {
296
- process.exit(1);
297
- }
298
- });