superacli 1.1.5 → 1.1.6

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/__tests__/resend-plugin.test.js +22 -1
  2. package/package.json +1 -1
  3. package/plugins/plugins.json +2 -2
  4. package/plugins/resend/plugin.json +279 -2
  5. package/plugins/resend/skills/quickstart/SKILL.md +32 -13
  6. package/temp_resend_cli/repo/.github/scripts/pr-title-check.js +34 -0
  7. package/temp_resend_cli/repo/.github/workflows/ci.yml +67 -0
  8. package/temp_resend_cli/repo/.github/workflows/post-release.yml +51 -0
  9. package/temp_resend_cli/repo/.github/workflows/pr-title-check.yml +13 -0
  10. package/temp_resend_cli/repo/.github/workflows/release.yml +175 -0
  11. package/temp_resend_cli/repo/.github/workflows/test-install-unix.yml +34 -0
  12. package/temp_resend_cli/repo/.github/workflows/test-install-windows.yml +48 -0
  13. package/temp_resend_cli/repo/CHANGELOG.md +31 -0
  14. package/temp_resend_cli/repo/LICENSE +21 -0
  15. package/temp_resend_cli/repo/README.md +450 -0
  16. package/temp_resend_cli/repo/biome.json +36 -0
  17. package/temp_resend_cli/repo/install.ps1 +141 -0
  18. package/temp_resend_cli/repo/install.sh +301 -0
  19. package/temp_resend_cli/repo/package.json +61 -0
  20. package/temp_resend_cli/repo/pnpm-lock.yaml +2439 -0
  21. package/temp_resend_cli/repo/renovate.json +4 -0
  22. package/temp_resend_cli/repo/src/cli.ts +98 -0
  23. package/temp_resend_cli/repo/src/commands/api-keys/create.ts +114 -0
  24. package/temp_resend_cli/repo/src/commands/api-keys/delete.ts +47 -0
  25. package/temp_resend_cli/repo/src/commands/api-keys/index.ts +26 -0
  26. package/temp_resend_cli/repo/src/commands/api-keys/list.ts +35 -0
  27. package/temp_resend_cli/repo/src/commands/api-keys/utils.ts +8 -0
  28. package/temp_resend_cli/repo/src/commands/auth/index.ts +20 -0
  29. package/temp_resend_cli/repo/src/commands/auth/login.ts +234 -0
  30. package/temp_resend_cli/repo/src/commands/auth/logout.ts +105 -0
  31. package/temp_resend_cli/repo/src/commands/broadcasts/create.ts +196 -0
  32. package/temp_resend_cli/repo/src/commands/broadcasts/delete.ts +46 -0
  33. package/temp_resend_cli/repo/src/commands/broadcasts/get.ts +59 -0
  34. package/temp_resend_cli/repo/src/commands/broadcasts/index.ts +43 -0
  35. package/temp_resend_cli/repo/src/commands/broadcasts/list.ts +60 -0
  36. package/temp_resend_cli/repo/src/commands/broadcasts/send.ts +56 -0
  37. package/temp_resend_cli/repo/src/commands/broadcasts/update.ts +95 -0
  38. package/temp_resend_cli/repo/src/commands/broadcasts/utils.ts +35 -0
  39. package/temp_resend_cli/repo/src/commands/contact-properties/create.ts +118 -0
  40. package/temp_resend_cli/repo/src/commands/contact-properties/delete.ts +48 -0
  41. package/temp_resend_cli/repo/src/commands/contact-properties/get.ts +46 -0
  42. package/temp_resend_cli/repo/src/commands/contact-properties/index.ts +48 -0
  43. package/temp_resend_cli/repo/src/commands/contact-properties/list.ts +68 -0
  44. package/temp_resend_cli/repo/src/commands/contact-properties/update.ts +88 -0
  45. package/temp_resend_cli/repo/src/commands/contact-properties/utils.ts +17 -0
  46. package/temp_resend_cli/repo/src/commands/contacts/add-segment.ts +78 -0
  47. package/temp_resend_cli/repo/src/commands/contacts/create.ts +122 -0
  48. package/temp_resend_cli/repo/src/commands/contacts/delete.ts +49 -0
  49. package/temp_resend_cli/repo/src/commands/contacts/get.ts +53 -0
  50. package/temp_resend_cli/repo/src/commands/contacts/index.ts +58 -0
  51. package/temp_resend_cli/repo/src/commands/contacts/list.ts +57 -0
  52. package/temp_resend_cli/repo/src/commands/contacts/remove-segment.ts +48 -0
  53. package/temp_resend_cli/repo/src/commands/contacts/segments.ts +39 -0
  54. package/temp_resend_cli/repo/src/commands/contacts/topics.ts +45 -0
  55. package/temp_resend_cli/repo/src/commands/contacts/update-topics.ts +90 -0
  56. package/temp_resend_cli/repo/src/commands/contacts/update.ts +77 -0
  57. package/temp_resend_cli/repo/src/commands/contacts/utils.ts +119 -0
  58. package/temp_resend_cli/repo/src/commands/doctor.ts +216 -0
  59. package/temp_resend_cli/repo/src/commands/domains/create.ts +83 -0
  60. package/temp_resend_cli/repo/src/commands/domains/delete.ts +42 -0
  61. package/temp_resend_cli/repo/src/commands/domains/get.ts +47 -0
  62. package/temp_resend_cli/repo/src/commands/domains/index.ts +35 -0
  63. package/temp_resend_cli/repo/src/commands/domains/list.ts +53 -0
  64. package/temp_resend_cli/repo/src/commands/domains/update.ts +75 -0
  65. package/temp_resend_cli/repo/src/commands/domains/utils.ts +44 -0
  66. package/temp_resend_cli/repo/src/commands/domains/verify.ts +38 -0
  67. package/temp_resend_cli/repo/src/commands/emails/batch.ts +140 -0
  68. package/temp_resend_cli/repo/src/commands/emails/get.ts +44 -0
  69. package/temp_resend_cli/repo/src/commands/emails/index.ts +30 -0
  70. package/temp_resend_cli/repo/src/commands/emails/list.ts +84 -0
  71. package/temp_resend_cli/repo/src/commands/emails/receiving/attachment.ts +55 -0
  72. package/temp_resend_cli/repo/src/commands/emails/receiving/attachments.ts +68 -0
  73. package/temp_resend_cli/repo/src/commands/emails/receiving/get.ts +58 -0
  74. package/temp_resend_cli/repo/src/commands/emails/receiving/index.ts +28 -0
  75. package/temp_resend_cli/repo/src/commands/emails/receiving/list.ts +59 -0
  76. package/temp_resend_cli/repo/src/commands/emails/receiving/utils.ts +38 -0
  77. package/temp_resend_cli/repo/src/commands/emails/send.ts +189 -0
  78. package/temp_resend_cli/repo/src/commands/open.ts +27 -0
  79. package/temp_resend_cli/repo/src/commands/segments/create.ts +50 -0
  80. package/temp_resend_cli/repo/src/commands/segments/delete.ts +47 -0
  81. package/temp_resend_cli/repo/src/commands/segments/get.ts +38 -0
  82. package/temp_resend_cli/repo/src/commands/segments/index.ts +36 -0
  83. package/temp_resend_cli/repo/src/commands/segments/list.ts +58 -0
  84. package/temp_resend_cli/repo/src/commands/segments/utils.ts +7 -0
  85. package/temp_resend_cli/repo/src/commands/teams/index.ts +10 -0
  86. package/temp_resend_cli/repo/src/commands/teams/list.ts +35 -0
  87. package/temp_resend_cli/repo/src/commands/teams/remove.ts +86 -0
  88. package/temp_resend_cli/repo/src/commands/teams/switch.ts +76 -0
  89. package/temp_resend_cli/repo/src/commands/topics/create.ts +73 -0
  90. package/temp_resend_cli/repo/src/commands/topics/delete.ts +47 -0
  91. package/temp_resend_cli/repo/src/commands/topics/get.ts +42 -0
  92. package/temp_resend_cli/repo/src/commands/topics/index.ts +42 -0
  93. package/temp_resend_cli/repo/src/commands/topics/list.ts +34 -0
  94. package/temp_resend_cli/repo/src/commands/topics/update.ts +59 -0
  95. package/temp_resend_cli/repo/src/commands/topics/utils.ts +16 -0
  96. package/temp_resend_cli/repo/src/commands/webhooks/create.ts +128 -0
  97. package/temp_resend_cli/repo/src/commands/webhooks/delete.ts +49 -0
  98. package/temp_resend_cli/repo/src/commands/webhooks/get.ts +42 -0
  99. package/temp_resend_cli/repo/src/commands/webhooks/index.ts +42 -0
  100. package/temp_resend_cli/repo/src/commands/webhooks/list.ts +55 -0
  101. package/temp_resend_cli/repo/src/commands/webhooks/listen.ts +379 -0
  102. package/temp_resend_cli/repo/src/commands/webhooks/update.ts +83 -0
  103. package/temp_resend_cli/repo/src/commands/webhooks/utils.ts +36 -0
  104. package/temp_resend_cli/repo/src/commands/whoami.ts +71 -0
  105. package/temp_resend_cli/repo/src/lib/actions.ts +157 -0
  106. package/temp_resend_cli/repo/src/lib/client.ts +37 -0
  107. package/temp_resend_cli/repo/src/lib/config.ts +217 -0
  108. package/temp_resend_cli/repo/src/lib/files.ts +15 -0
  109. package/temp_resend_cli/repo/src/lib/help-text.ts +38 -0
  110. package/temp_resend_cli/repo/src/lib/output.ts +56 -0
  111. package/temp_resend_cli/repo/src/lib/pagination.ts +36 -0
  112. package/temp_resend_cli/repo/src/lib/prompts.ts +149 -0
  113. package/temp_resend_cli/repo/src/lib/spinner.ts +100 -0
  114. package/temp_resend_cli/repo/src/lib/table.ts +57 -0
  115. package/temp_resend_cli/repo/src/lib/tty.ts +28 -0
  116. package/temp_resend_cli/repo/src/lib/update-check.ts +169 -0
  117. package/temp_resend_cli/repo/src/lib/version.ts +4 -0
  118. package/temp_resend_cli/repo/tests/commands/api-keys/create.test.ts +196 -0
  119. package/temp_resend_cli/repo/tests/commands/api-keys/delete.test.ts +157 -0
  120. package/temp_resend_cli/repo/tests/commands/api-keys/list.test.ts +134 -0
  121. package/temp_resend_cli/repo/tests/commands/auth/login.test.ts +153 -0
  122. package/temp_resend_cli/repo/tests/commands/auth/logout.test.ts +153 -0
  123. package/temp_resend_cli/repo/tests/commands/broadcasts/create.test.ts +454 -0
  124. package/temp_resend_cli/repo/tests/commands/broadcasts/delete.test.ts +183 -0
  125. package/temp_resend_cli/repo/tests/commands/broadcasts/get.test.ts +147 -0
  126. package/temp_resend_cli/repo/tests/commands/broadcasts/list.test.ts +199 -0
  127. package/temp_resend_cli/repo/tests/commands/broadcasts/send.test.ts +162 -0
  128. package/temp_resend_cli/repo/tests/commands/broadcasts/update.test.ts +288 -0
  129. package/temp_resend_cli/repo/tests/commands/contact-properties/create.test.ts +251 -0
  130. package/temp_resend_cli/repo/tests/commands/contact-properties/delete.test.ts +184 -0
  131. package/temp_resend_cli/repo/tests/commands/contact-properties/get.test.ts +145 -0
  132. package/temp_resend_cli/repo/tests/commands/contact-properties/list.test.ts +181 -0
  133. package/temp_resend_cli/repo/tests/commands/contact-properties/update.test.ts +217 -0
  134. package/temp_resend_cli/repo/tests/commands/contacts/add-segment.test.ts +189 -0
  135. package/temp_resend_cli/repo/tests/commands/contacts/create.test.ts +271 -0
  136. package/temp_resend_cli/repo/tests/commands/contacts/delete.test.ts +193 -0
  137. package/temp_resend_cli/repo/tests/commands/contacts/get.test.ts +149 -0
  138. package/temp_resend_cli/repo/tests/commands/contacts/list.test.ts +176 -0
  139. package/temp_resend_cli/repo/tests/commands/contacts/remove-segment.test.ts +167 -0
  140. package/temp_resend_cli/repo/tests/commands/contacts/segments.test.ts +168 -0
  141. package/temp_resend_cli/repo/tests/commands/contacts/topics.test.ts +164 -0
  142. package/temp_resend_cli/repo/tests/commands/contacts/update-topics.test.ts +248 -0
  143. package/temp_resend_cli/repo/tests/commands/contacts/update.test.ts +206 -0
  144. package/temp_resend_cli/repo/tests/commands/doctor.test.ts +164 -0
  145. package/temp_resend_cli/repo/tests/commands/domains/create.test.ts +193 -0
  146. package/temp_resend_cli/repo/tests/commands/domains/delete.test.ts +157 -0
  147. package/temp_resend_cli/repo/tests/commands/domains/get.test.ts +138 -0
  148. package/temp_resend_cli/repo/tests/commands/domains/list.test.ts +165 -0
  149. package/temp_resend_cli/repo/tests/commands/domains/update.test.ts +224 -0
  150. package/temp_resend_cli/repo/tests/commands/domains/verify.test.ts +118 -0
  151. package/temp_resend_cli/repo/tests/commands/emails/batch.test.ts +324 -0
  152. package/temp_resend_cli/repo/tests/commands/emails/get.test.ts +132 -0
  153. package/temp_resend_cli/repo/tests/commands/emails/receiving/attachment.test.ts +141 -0
  154. package/temp_resend_cli/repo/tests/commands/emails/receiving/attachments.test.ts +169 -0
  155. package/temp_resend_cli/repo/tests/commands/emails/receiving/get.test.ts +141 -0
  156. package/temp_resend_cli/repo/tests/commands/emails/receiving/list.test.ts +182 -0
  157. package/temp_resend_cli/repo/tests/commands/emails/send.test.ts +312 -0
  158. package/temp_resend_cli/repo/tests/commands/segments/create.test.ts +164 -0
  159. package/temp_resend_cli/repo/tests/commands/segments/delete.test.ts +183 -0
  160. package/temp_resend_cli/repo/tests/commands/segments/get.test.ts +138 -0
  161. package/temp_resend_cli/repo/tests/commands/segments/list.test.ts +174 -0
  162. package/temp_resend_cli/repo/tests/commands/teams/list.test.ts +62 -0
  163. package/temp_resend_cli/repo/tests/commands/teams/remove.test.ts +110 -0
  164. package/temp_resend_cli/repo/tests/commands/teams/switch.test.ts +103 -0
  165. package/temp_resend_cli/repo/tests/commands/topics/create.test.ts +192 -0
  166. package/temp_resend_cli/repo/tests/commands/topics/delete.test.ts +157 -0
  167. package/temp_resend_cli/repo/tests/commands/topics/get.test.ts +126 -0
  168. package/temp_resend_cli/repo/tests/commands/topics/list.test.ts +125 -0
  169. package/temp_resend_cli/repo/tests/commands/topics/update.test.ts +178 -0
  170. package/temp_resend_cli/repo/tests/commands/webhooks/create.test.ts +225 -0
  171. package/temp_resend_cli/repo/tests/commands/webhooks/delete.test.ts +157 -0
  172. package/temp_resend_cli/repo/tests/commands/webhooks/get.test.ts +126 -0
  173. package/temp_resend_cli/repo/tests/commands/webhooks/list.test.ts +178 -0
  174. package/temp_resend_cli/repo/tests/commands/webhooks/update.test.ts +207 -0
  175. package/temp_resend_cli/repo/tests/commands/whoami.test.ts +98 -0
  176. package/temp_resend_cli/repo/tests/e2e/smoke.test.ts +93 -0
  177. package/temp_resend_cli/repo/tests/helpers.ts +86 -0
  178. package/temp_resend_cli/repo/tests/lib/client.test.ts +71 -0
  179. package/temp_resend_cli/repo/tests/lib/config.test.ts +451 -0
  180. package/temp_resend_cli/repo/tests/lib/files.test.ts +73 -0
  181. package/temp_resend_cli/repo/tests/lib/help-text.test.ts +97 -0
  182. package/temp_resend_cli/repo/tests/lib/output.test.ts +136 -0
  183. package/temp_resend_cli/repo/tests/lib/prompts.test.ts +185 -0
  184. package/temp_resend_cli/repo/tests/lib/spinner.test.ts +166 -0
  185. package/temp_resend_cli/repo/tests/lib/table.test.ts +63 -0
  186. package/temp_resend_cli/repo/tests/lib/tty.test.ts +89 -0
  187. package/temp_resend_cli/repo/tests/lib/update-check.test.ts +179 -0
  188. package/temp_resend_cli/repo/tsconfig.json +14 -0
  189. package/temp_resend_cli/repo/vitest.config.e2e.ts +8 -0
  190. package/temp_resend_cli/repo/vitest.config.ts +10 -0
  191. package/tests/test-resend-smoke.sh +7 -3
@@ -0,0 +1,379 @@
1
+ import {
2
+ createServer,
3
+ type IncomingMessage,
4
+ type ServerResponse,
5
+ } from 'node:http';
6
+ import { Command } from '@commander-js/extra-typings';
7
+ import pc from 'picocolors';
8
+ import type { Resend, WebhookEvent } from 'resend';
9
+ import type { GlobalOpts } from '../../lib/client';
10
+ import { requireClient } from '../../lib/client';
11
+ import { buildHelpText } from '../../lib/help-text';
12
+ import { outputError } from '../../lib/output';
13
+ import { requireText } from '../../lib/prompts';
14
+ import { createSpinner } from '../../lib/spinner';
15
+ import { isInteractive } from '../../lib/tty';
16
+ import { ALL_WEBHOOK_EVENTS } from './utils';
17
+
18
+ const SVIX_HEADERS = ['svix-id', 'svix-timestamp', 'svix-signature'];
19
+
20
+ function timestamp(): string {
21
+ return new Date().toLocaleTimeString('en-GB', { hour12: false });
22
+ }
23
+
24
+ function summarizeEvent(body: Record<string, unknown>): {
25
+ type: string;
26
+ resourceId: string;
27
+ detail: string;
28
+ } {
29
+ const type = (body.type as string) ?? 'unknown';
30
+ const data = (body.data as Record<string, unknown>) ?? {};
31
+
32
+ const resourceId = (data.id as string) ?? '';
33
+
34
+ let detail = '';
35
+ if (type.startsWith('email.')) {
36
+ const from = (data.from as string) ?? '';
37
+ const to = Array.isArray(data.to) ? (data.to[0] as string) : '';
38
+ if (from || to) {
39
+ detail = `${from} -> ${to}`;
40
+ }
41
+ } else if (type.startsWith('domain.')) {
42
+ detail = (data.name as string) ?? '';
43
+ } else if (type.startsWith('contact.')) {
44
+ detail = (data.email as string) ?? '';
45
+ }
46
+
47
+ return { type, resourceId, detail };
48
+ }
49
+
50
+ function formatStatus(status: number): string {
51
+ const text = `${status} ${statusText(status)}`;
52
+ if (status >= 200 && status < 300) {
53
+ return pc.green(text);
54
+ }
55
+ if (status >= 400 && status < 500) {
56
+ return pc.yellow(text);
57
+ }
58
+ return pc.red(text);
59
+ }
60
+
61
+ function statusText(code: number): string {
62
+ const map: Record<number, string> = {
63
+ 200: 'OK',
64
+ 201: 'Created',
65
+ 204: 'No Content',
66
+ 400: 'Bad Request',
67
+ 401: 'Unauthorized',
68
+ 403: 'Forbidden',
69
+ 404: 'Not Found',
70
+ 500: 'Internal Server Error',
71
+ 502: 'Bad Gateway',
72
+ 503: 'Service Unavailable',
73
+ };
74
+ return map[code] ?? '';
75
+ }
76
+
77
+ async function forwardPayload(
78
+ forwardTo: string,
79
+ rawBody: string,
80
+ headers: Record<string, string | undefined>,
81
+ ): Promise<{ status: number }> {
82
+ const forwardHeaders: Record<string, string> = {
83
+ 'content-type': 'application/json',
84
+ };
85
+ for (const h of SVIX_HEADERS) {
86
+ const val = headers[h];
87
+ if (val) {
88
+ forwardHeaders[h] = val;
89
+ }
90
+ }
91
+
92
+ const url = forwardTo.startsWith('http') ? forwardTo : `http://${forwardTo}`;
93
+ const resp = await fetch(url, {
94
+ method: 'POST',
95
+ headers: forwardHeaders,
96
+ body: rawBody,
97
+ });
98
+ return { status: resp.status };
99
+ }
100
+
101
+ function readBody(req: IncomingMessage): Promise<string> {
102
+ return new Promise((resolve, reject) => {
103
+ const chunks: Buffer[] = [];
104
+ req.on('data', (chunk: Buffer) => chunks.push(chunk));
105
+ req.on('end', () => resolve(Buffer.concat(chunks).toString()));
106
+ req.on('error', reject);
107
+ });
108
+ }
109
+
110
+ async function cleanup(
111
+ resend: Resend,
112
+ webhookId: string,
113
+ server: ReturnType<typeof createServer>,
114
+ ) {
115
+ process.stderr.write('\nCleaning up...');
116
+ try {
117
+ await resend.webhooks.remove(webhookId);
118
+ process.stderr.write(` Webhook ${webhookId} deleted.\n`);
119
+ } catch {
120
+ process.stderr.write(` Failed to delete webhook ${webhookId}.\n`);
121
+ }
122
+ server.close();
123
+ }
124
+
125
+ export const listenWebhookCommand = new Command('listen')
126
+ .description('Listen for webhook events locally during development')
127
+ .option('--url <url>', 'Public URL for receiving webhooks (your tunnel URL)')
128
+ .option('--forward-to <url>', 'Forward payloads to this local URL')
129
+ .option('--events <events...>', 'Event types to listen for (default: all)')
130
+ .option('--port <port>', 'Local server port', '4318')
131
+ .addHelpText(
132
+ 'after',
133
+ buildHelpText({
134
+ context: `Start a local server that receives Resend webhook events in real time.
135
+
136
+ You must provide a public URL (e.g. from ngrok or localtunnel) that
137
+ points to the local server port. The CLI will:
138
+ 1. Start a local HTTP server on --port (default 4318)
139
+ 2. Register a temporary Resend webhook pointing at --url
140
+ 3. Display incoming events in the terminal
141
+ 4. Optionally forward payloads to --forward-to (with original Svix headers)
142
+ 5. Delete the temporary webhook on exit (Ctrl+C)
143
+
144
+ Important: your tunnel must forward traffic to the same port as --port (default 4318).
145
+ For example, if using ngrok: ngrok http 4318`,
146
+ examples: [
147
+ 'resend webhooks listen --url https://example.ngrok-free.app',
148
+ 'resend webhooks listen --url https://example.ngrok-free.app --forward-to localhost:3000/webhook',
149
+ 'resend webhooks listen --url https://example.ngrok-free.app --events email.sent email.bounced',
150
+ 'resend webhooks listen --url https://example.ngrok-free.app --port 8080',
151
+ ],
152
+ }),
153
+ )
154
+ .action(async (opts, cmd) => {
155
+ const globalOpts = cmd.optsWithGlobals() as GlobalOpts;
156
+
157
+ const url = await requireText(
158
+ opts.url,
159
+ {
160
+ message: 'Public tunnel URL',
161
+ placeholder: 'https://example.ngrok-free.app',
162
+ validate: (v) => {
163
+ if (!v) {
164
+ return 'URL is required';
165
+ }
166
+ if (!v.startsWith('https://') && !v.startsWith('http://')) {
167
+ return 'URL must start with http:// or https://';
168
+ }
169
+ return undefined;
170
+ },
171
+ },
172
+ { message: 'Missing --url flag.', code: 'missing_url' },
173
+ globalOpts,
174
+ );
175
+
176
+ const port = Number.parseInt(opts.port ?? '4318', 10);
177
+ if (Number.isNaN(port) || port < 1 || port > 65535) {
178
+ outputError(
179
+ { message: 'Invalid port number.', code: 'invalid_port' },
180
+ { json: globalOpts.json },
181
+ );
182
+ }
183
+
184
+ let selectedEvents: WebhookEvent[];
185
+ if (opts.events?.includes('all')) {
186
+ selectedEvents = ALL_WEBHOOK_EVENTS;
187
+ } else if (opts.events?.length) {
188
+ selectedEvents = opts.events as WebhookEvent[];
189
+ } else {
190
+ selectedEvents = ALL_WEBHOOK_EVENTS;
191
+ }
192
+
193
+ const resend = requireClient(globalOpts);
194
+ const jsonMode = globalOpts.json || !isInteractive();
195
+
196
+ // Start local server
197
+ const server = createServer(
198
+ async (req: IncomingMessage, res: ServerResponse) => {
199
+ if (req.method !== 'POST') {
200
+ res.writeHead(405).end('Method not allowed');
201
+ return;
202
+ }
203
+
204
+ const rawBody = await readBody(req);
205
+ let body: Record<string, unknown>;
206
+ try {
207
+ body = JSON.parse(rawBody);
208
+ } catch {
209
+ res.writeHead(400).end('Invalid JSON');
210
+ return;
211
+ }
212
+
213
+ const svixHeaders: Record<string, string | undefined> = {};
214
+ for (const h of SVIX_HEADERS) {
215
+ const val = req.headers[h];
216
+ svixHeaders[h] = Array.isArray(val) ? val[0] : val;
217
+ }
218
+
219
+ const { type, resourceId, detail } = summarizeEvent(body);
220
+
221
+ if (jsonMode) {
222
+ const entry: Record<string, unknown> = {
223
+ timestamp: new Date().toISOString(),
224
+ type,
225
+ resource_id: resourceId,
226
+ payload: body,
227
+ };
228
+
229
+ if (opts.forwardTo) {
230
+ try {
231
+ const { status } = await forwardPayload(
232
+ opts.forwardTo,
233
+ rawBody,
234
+ svixHeaders,
235
+ );
236
+ entry.forwarded = { url: opts.forwardTo, status };
237
+ } catch (err) {
238
+ entry.forwarded = {
239
+ url: opts.forwardTo,
240
+ error: err instanceof Error ? err.message : 'Unknown error',
241
+ };
242
+ }
243
+ }
244
+
245
+ console.log(JSON.stringify(entry));
246
+ } else {
247
+ const ts = pc.dim(`[${timestamp()}]`);
248
+ const typePad = type.padEnd(20);
249
+ const idPad = resourceId.padEnd(14);
250
+ process.stderr.write(
251
+ `${ts} ${pc.bold(typePad)} ${pc.cyan(idPad)} ${detail}\n`,
252
+ );
253
+
254
+ if (opts.forwardTo) {
255
+ const target = opts.forwardTo.startsWith('http')
256
+ ? opts.forwardTo
257
+ : `http://${opts.forwardTo}`;
258
+ try {
259
+ const { status } = await forwardPayload(
260
+ opts.forwardTo,
261
+ rawBody,
262
+ svixHeaders,
263
+ );
264
+ process.stderr.write(
265
+ `${pc.dim(' -> POST')} ${target} ${pc.dim(`[${formatStatus(status)}]`)}\n`,
266
+ );
267
+ } catch (err) {
268
+ const msg = err instanceof Error ? err.message : 'Unknown error';
269
+ process.stderr.write(
270
+ `${pc.dim(' -> POST')} ${target} ${pc.red(`[Error: ${msg}]`)}\n`,
271
+ );
272
+ }
273
+ }
274
+ }
275
+
276
+ res.writeHead(200).end('OK');
277
+ },
278
+ );
279
+
280
+ try {
281
+ await new Promise<void>((resolve, reject) => {
282
+ server.on('error', reject);
283
+ server.listen(port, resolve);
284
+ });
285
+ } catch (err) {
286
+ outputError(
287
+ {
288
+ message:
289
+ err instanceof Error
290
+ ? `Failed to start local server on port ${port}: ${err.message}`
291
+ : `Failed to start local server on port ${port}`,
292
+ code: 'server_listen_error',
293
+ },
294
+ { json: globalOpts.json },
295
+ );
296
+ }
297
+
298
+ // Register webhook
299
+ const spinner = createSpinner(
300
+ 'Setting up webhook listener...',
301
+ globalOpts.quiet,
302
+ );
303
+
304
+ let webhookId: string;
305
+ try {
306
+ const { data, error } = await resend.webhooks.create({
307
+ endpoint: url,
308
+ events: selectedEvents,
309
+ });
310
+ if (error || !data) {
311
+ spinner.fail('Failed to create webhook');
312
+ server.close();
313
+ outputError(
314
+ {
315
+ message: error?.message ?? 'Unexpected empty response',
316
+ code: 'create_error',
317
+ },
318
+ { json: globalOpts.json },
319
+ );
320
+ }
321
+ webhookId = data.id;
322
+ } catch (err) {
323
+ spinner.fail('Failed to create webhook');
324
+ server.close();
325
+ outputError(
326
+ {
327
+ message: err instanceof Error ? err.message : 'Unknown error',
328
+ code: 'create_error',
329
+ },
330
+ { json: globalOpts.json },
331
+ );
332
+ }
333
+
334
+ spinner.stop('Webhook listener ready');
335
+
336
+ // Print banner
337
+ if (!jsonMode) {
338
+ const eventsDisplay =
339
+ selectedEvents.length === ALL_WEBHOOK_EVENTS.length
340
+ ? 'all events'
341
+ : selectedEvents.length <= 3
342
+ ? selectedEvents.join(', ')
343
+ : `${selectedEvents.slice(0, 3).join(', ')} (+${selectedEvents.length - 3} more)`;
344
+
345
+ process.stderr.write('\n');
346
+ process.stderr.write(` ${pc.bold('Webhook:')} ${webhookId}\n`);
347
+ process.stderr.write(` ${pc.bold('Endpoint:')} ${url}\n`);
348
+ process.stderr.write(` ${pc.bold('Events:')} ${eventsDisplay}\n`);
349
+ process.stderr.write(
350
+ ` ${pc.bold('Server:')} http://localhost:${port}\n`,
351
+ );
352
+ if (opts.forwardTo) {
353
+ const fwd = opts.forwardTo.startsWith('http')
354
+ ? opts.forwardTo
355
+ : `http://${opts.forwardTo}`;
356
+ process.stderr.write(` ${pc.bold('Forward:')} ${fwd}\n`);
357
+ }
358
+ process.stderr.write(
359
+ `\nReady! Listening for webhook events. Press Ctrl+C to stop.\n\n`,
360
+ );
361
+ }
362
+
363
+ // Cleanup handler
364
+ let cleaningUp = false;
365
+ const handleSignal = async () => {
366
+ if (cleaningUp) {
367
+ return;
368
+ }
369
+ cleaningUp = true;
370
+ await cleanup(resend, webhookId, server);
371
+ process.exit(0);
372
+ };
373
+
374
+ process.on('SIGINT', handleSignal);
375
+ process.on('SIGTERM', handleSignal);
376
+
377
+ // Keep the process alive until signal
378
+ await new Promise(() => {});
379
+ });
@@ -0,0 +1,83 @@
1
+ import { Command, Option } from '@commander-js/extra-typings';
2
+ import type { WebhookEvent } from 'resend';
3
+ import { runWrite } from '../../lib/actions';
4
+ import type { GlobalOpts } from '../../lib/client';
5
+ import { buildHelpText } from '../../lib/help-text';
6
+ import { outputError } from '../../lib/output';
7
+ import { ALL_WEBHOOK_EVENTS } from './utils';
8
+
9
+ export const updateWebhookCommand = new Command('update')
10
+ .description(
11
+ "Update a webhook's endpoint URL, event subscriptions, or enabled status",
12
+ )
13
+ .argument('<id>', 'Webhook UUID')
14
+ .option('--endpoint <endpoint>', 'New HTTPS URL for this webhook')
15
+ .option(
16
+ '--events <events...>',
17
+ 'Replace the full event subscription list. Use "all" for all 17 events.',
18
+ )
19
+ .addOption(
20
+ new Option(
21
+ '--status <status>',
22
+ 'Enable or disable event delivery for this webhook',
23
+ ).choices(['enabled', 'disabled'] as const),
24
+ )
25
+ .addHelpText(
26
+ 'after',
27
+ buildHelpText({
28
+ context: `At least one of --endpoint, --events, or --status must be provided.
29
+
30
+ --events replaces the entire event list (it is not additive).
31
+ Use "all" as a shorthand for all 17 event types.
32
+
33
+ --status controls whether events are delivered to this endpoint:
34
+ enabled Events are delivered (default on creation)
35
+ disabled Events are suppressed without deleting the webhook`,
36
+ output: ` {"object":"webhook","id":"<uuid>"}`,
37
+ errorCodes: ['auth_error', 'no_changes', 'update_error'],
38
+ examples: [
39
+ 'resend webhooks update wh_abc123 --endpoint https://new-app.example.com/hooks/resend',
40
+ 'resend webhooks update wh_abc123 --events email.sent email.bounced',
41
+ 'resend webhooks update wh_abc123 --events all',
42
+ 'resend webhooks update wh_abc123 --status disabled',
43
+ 'resend webhooks update wh_abc123 --endpoint https://new-app.example.com/hooks/resend --events all --json',
44
+ ],
45
+ }),
46
+ )
47
+ .action(async (id, opts, cmd) => {
48
+ const globalOpts = cmd.optsWithGlobals() as GlobalOpts;
49
+
50
+ if (!opts.endpoint && !opts.events?.length && !opts.status) {
51
+ outputError(
52
+ {
53
+ message:
54
+ 'Provide at least one option to update: --endpoint, --events, or --status.',
55
+ code: 'no_changes',
56
+ },
57
+ { json: globalOpts.json },
58
+ );
59
+ }
60
+
61
+ const selectedEvents = opts.events?.includes('all')
62
+ ? ALL_WEBHOOK_EVENTS
63
+ : (opts.events as WebhookEvent[] | undefined);
64
+
65
+ await runWrite(
66
+ {
67
+ spinner: {
68
+ loading: 'Updating webhook...',
69
+ success: 'Webhook updated',
70
+ fail: 'Failed to update webhook',
71
+ },
72
+ sdkCall: (resend) =>
73
+ resend.webhooks.update(id, {
74
+ ...(opts.endpoint && { endpoint: opts.endpoint }),
75
+ ...(selectedEvents?.length && { events: selectedEvents }),
76
+ ...(opts.status && { status: opts.status }),
77
+ }),
78
+ errorCode: 'update_error',
79
+ successMsg: `Webhook updated: ${id}`,
80
+ },
81
+ globalOpts,
82
+ );
83
+ });
@@ -0,0 +1,36 @@
1
+ import type { Webhook, WebhookEvent } from 'resend';
2
+ import { renderTable } from '../../lib/table';
3
+
4
+ export const ALL_WEBHOOK_EVENTS: WebhookEvent[] = [
5
+ 'email.sent',
6
+ 'email.delivered',
7
+ 'email.delivery_delayed',
8
+ 'email.bounced',
9
+ 'email.complained',
10
+ 'email.opened',
11
+ 'email.clicked',
12
+ 'email.failed',
13
+ 'email.scheduled',
14
+ 'email.suppressed',
15
+ 'email.received',
16
+ 'contact.created',
17
+ 'contact.updated',
18
+ 'contact.deleted',
19
+ 'domain.created',
20
+ 'domain.updated',
21
+ 'domain.deleted',
22
+ ];
23
+
24
+ export function renderWebhooksTable(webhooks: Webhook[]): string {
25
+ const rows = webhooks.map((w) => {
26
+ const eventsStr = (w.events ?? []).join(', ');
27
+ const events =
28
+ eventsStr.length > 60 ? `${eventsStr.slice(0, 57)}...` : eventsStr;
29
+ return [w.endpoint, events, w.status, w.id];
30
+ });
31
+ return renderTable(
32
+ ['Endpoint', 'Events', 'Status', 'ID'],
33
+ rows,
34
+ '(no webhooks)',
35
+ );
36
+ }
@@ -0,0 +1,71 @@
1
+ import { Command } from '@commander-js/extra-typings';
2
+ import type { GlobalOpts } from '../lib/client';
3
+ import { maskKey, resolveApiKey, resolveTeamName } from '../lib/config';
4
+ import { buildHelpText } from '../lib/help-text';
5
+ import { outputError, outputResult } from '../lib/output';
6
+ import { isInteractive } from '../lib/tty';
7
+
8
+ export const whoamiCommand = new Command('whoami')
9
+ .description('Show current authentication status')
10
+ .addHelpText(
11
+ 'after',
12
+ buildHelpText({
13
+ setup: true,
14
+ context: `Local only — no network calls.
15
+ Shows which team is active and where the API key comes from.`,
16
+ output: ` {"authenticated":true,"team":"production","api_key":"re_...abcd","source":"config"}
17
+ {"authenticated":false}`,
18
+ examples: [
19
+ 'resend whoami',
20
+ 'resend whoami --json',
21
+ 'resend whoami --team production',
22
+ ],
23
+ }),
24
+ )
25
+ .action((_opts, cmd) => {
26
+ const globalOpts = cmd.optsWithGlobals() as GlobalOpts;
27
+ const teamName = globalOpts.team;
28
+ const resolved = resolveApiKey(globalOpts.apiKey, teamName);
29
+
30
+ if (!resolved) {
31
+ if (globalOpts.json || !isInteractive()) {
32
+ outputResult(
33
+ { authenticated: false },
34
+ { json: globalOpts.json, exitCode: 1 },
35
+ );
36
+ // outputResult with exitCode calls process.exit, but TS doesn't know
37
+ return;
38
+ }
39
+ outputError(
40
+ {
41
+ message: 'Not authenticated.\nRun `resend login` to get started.',
42
+ code: 'not_authenticated',
43
+ },
44
+ { json: false },
45
+ );
46
+ return;
47
+ }
48
+
49
+ const team = resolved.team ?? resolveTeamName(teamName);
50
+
51
+ if (globalOpts.json || !isInteractive()) {
52
+ outputResult(
53
+ {
54
+ authenticated: true,
55
+ team,
56
+ api_key: maskKey(resolved.key),
57
+ source: resolved.source,
58
+ },
59
+ { json: globalOpts.json },
60
+ );
61
+ return;
62
+ }
63
+
64
+ console.log('');
65
+ console.log(` Team: ${team}`);
66
+ console.log(` API Key: ${maskKey(resolved.key)}`);
67
+ console.log(
68
+ ` Source: ${resolved.source === 'config' ? 'config file' : resolved.source === 'env' ? 'environment variable' : 'flag'}`,
69
+ );
70
+ console.log('');
71
+ });