resend-cli 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/.claude/settings.local.json +14 -0
  2. package/.github/scripts/pr-title-check.js +34 -0
  3. package/.github/workflows/ci.yml +32 -0
  4. package/.github/workflows/pr-title-check.yml +13 -0
  5. package/.github/workflows/release.yml +93 -0
  6. package/CHANGELOG.md +31 -0
  7. package/LICENSE +21 -21
  8. package/README.md +416 -19
  9. package/biome.json +36 -0
  10. package/bun.lock +76 -0
  11. package/bunfig.toml +2 -0
  12. package/install.ps1 +140 -0
  13. package/install.sh +294 -0
  14. package/package.json +43 -22
  15. package/src/cli.ts +65 -0
  16. package/src/commands/api-keys/create.ts +114 -0
  17. package/src/commands/api-keys/delete.ts +47 -0
  18. package/src/commands/api-keys/index.ts +26 -0
  19. package/src/commands/api-keys/list.ts +35 -0
  20. package/src/commands/api-keys/utils.ts +8 -0
  21. package/src/commands/auth/index.ts +20 -0
  22. package/src/commands/auth/login.ts +211 -0
  23. package/src/commands/auth/logout.ts +105 -0
  24. package/src/commands/broadcasts/create.ts +196 -0
  25. package/src/commands/broadcasts/delete.ts +46 -0
  26. package/src/commands/broadcasts/get.ts +59 -0
  27. package/src/commands/broadcasts/index.ts +43 -0
  28. package/src/commands/broadcasts/list.ts +60 -0
  29. package/src/commands/broadcasts/send.ts +56 -0
  30. package/src/commands/broadcasts/update.ts +95 -0
  31. package/src/commands/broadcasts/utils.ts +35 -0
  32. package/src/commands/contact-properties/create.ts +118 -0
  33. package/src/commands/contact-properties/delete.ts +48 -0
  34. package/src/commands/contact-properties/get.ts +46 -0
  35. package/src/commands/contact-properties/index.ts +48 -0
  36. package/src/commands/contact-properties/list.ts +68 -0
  37. package/src/commands/contact-properties/update.ts +88 -0
  38. package/src/commands/contact-properties/utils.ts +17 -0
  39. package/src/commands/contacts/add-segment.ts +78 -0
  40. package/src/commands/contacts/create.ts +122 -0
  41. package/src/commands/contacts/delete.ts +49 -0
  42. package/src/commands/contacts/get.ts +53 -0
  43. package/src/commands/contacts/index.ts +58 -0
  44. package/src/commands/contacts/list.ts +57 -0
  45. package/src/commands/contacts/remove-segment.ts +48 -0
  46. package/src/commands/contacts/segments.ts +39 -0
  47. package/src/commands/contacts/topics.ts +45 -0
  48. package/src/commands/contacts/update-topics.ts +90 -0
  49. package/src/commands/contacts/update.ts +77 -0
  50. package/src/commands/contacts/utils.ts +119 -0
  51. package/src/commands/doctor.ts +298 -0
  52. package/src/commands/domains/create.ts +83 -0
  53. package/src/commands/domains/delete.ts +42 -0
  54. package/src/commands/domains/get.ts +47 -0
  55. package/src/commands/domains/index.ts +35 -0
  56. package/src/commands/domains/list.ts +53 -0
  57. package/src/commands/domains/update.ts +75 -0
  58. package/src/commands/domains/utils.ts +44 -0
  59. package/src/commands/domains/verify.ts +38 -0
  60. package/src/commands/emails/batch.ts +140 -0
  61. package/src/commands/emails/index.ts +24 -0
  62. package/src/commands/emails/receiving/attachment.ts +55 -0
  63. package/src/commands/emails/receiving/attachments.ts +68 -0
  64. package/src/commands/emails/receiving/get.ts +58 -0
  65. package/src/commands/emails/receiving/index.ts +28 -0
  66. package/src/commands/emails/receiving/list.ts +59 -0
  67. package/src/commands/emails/receiving/utils.ts +38 -0
  68. package/src/commands/emails/send.ts +189 -0
  69. package/src/commands/segments/create.ts +50 -0
  70. package/src/commands/segments/delete.ts +47 -0
  71. package/src/commands/segments/get.ts +38 -0
  72. package/src/commands/segments/index.ts +36 -0
  73. package/src/commands/segments/list.ts +58 -0
  74. package/src/commands/segments/utils.ts +7 -0
  75. package/src/commands/teams/index.ts +10 -0
  76. package/src/commands/teams/list.ts +35 -0
  77. package/src/commands/teams/remove.ts +83 -0
  78. package/src/commands/teams/switch.ts +73 -0
  79. package/src/commands/topics/create.ts +73 -0
  80. package/src/commands/topics/delete.ts +47 -0
  81. package/src/commands/topics/get.ts +42 -0
  82. package/src/commands/topics/index.ts +42 -0
  83. package/src/commands/topics/list.ts +34 -0
  84. package/src/commands/topics/update.ts +59 -0
  85. package/src/commands/topics/utils.ts +16 -0
  86. package/src/commands/webhooks/create.ts +128 -0
  87. package/src/commands/webhooks/delete.ts +49 -0
  88. package/src/commands/webhooks/get.ts +42 -0
  89. package/src/commands/webhooks/index.ts +44 -0
  90. package/src/commands/webhooks/list.ts +55 -0
  91. package/src/commands/webhooks/update.ts +83 -0
  92. package/src/commands/webhooks/utils.ts +36 -0
  93. package/src/commands/whoami.ts +71 -0
  94. package/src/lib/actions.ts +157 -0
  95. package/src/lib/client.ts +29 -0
  96. package/src/lib/config.ts +211 -0
  97. package/src/lib/files.ts +15 -0
  98. package/src/lib/help-text.ts +36 -0
  99. package/src/lib/output.ts +54 -0
  100. package/src/lib/pagination.ts +36 -0
  101. package/src/lib/prompts.ts +149 -0
  102. package/src/lib/spinner.ts +89 -0
  103. package/src/lib/table.ts +57 -0
  104. package/src/lib/tty.ts +28 -0
  105. package/src/lib/version.ts +4 -0
  106. package/tests/commands/api-keys/create.test.ts +195 -0
  107. package/tests/commands/api-keys/delete.test.ts +156 -0
  108. package/tests/commands/api-keys/list.test.ts +133 -0
  109. package/tests/commands/auth/login.test.ts +119 -0
  110. package/tests/commands/auth/logout.test.ts +146 -0
  111. package/tests/commands/broadcasts/create.test.ts +447 -0
  112. package/tests/commands/broadcasts/delete.test.ts +182 -0
  113. package/tests/commands/broadcasts/get.test.ts +146 -0
  114. package/tests/commands/broadcasts/list.test.ts +196 -0
  115. package/tests/commands/broadcasts/send.test.ts +161 -0
  116. package/tests/commands/broadcasts/update.test.ts +283 -0
  117. package/tests/commands/contact-properties/create.test.ts +250 -0
  118. package/tests/commands/contact-properties/delete.test.ts +183 -0
  119. package/tests/commands/contact-properties/get.test.ts +144 -0
  120. package/tests/commands/contact-properties/list.test.ts +180 -0
  121. package/tests/commands/contact-properties/update.test.ts +216 -0
  122. package/tests/commands/contacts/add-segment.test.ts +188 -0
  123. package/tests/commands/contacts/create.test.ts +270 -0
  124. package/tests/commands/contacts/delete.test.ts +192 -0
  125. package/tests/commands/contacts/get.test.ts +148 -0
  126. package/tests/commands/contacts/list.test.ts +175 -0
  127. package/tests/commands/contacts/remove-segment.test.ts +166 -0
  128. package/tests/commands/contacts/segments.test.ts +167 -0
  129. package/tests/commands/contacts/topics.test.ts +163 -0
  130. package/tests/commands/contacts/update-topics.test.ts +247 -0
  131. package/tests/commands/contacts/update.test.ts +205 -0
  132. package/tests/commands/doctor.test.ts +165 -0
  133. package/tests/commands/domains/create.test.ts +192 -0
  134. package/tests/commands/domains/delete.test.ts +156 -0
  135. package/tests/commands/domains/get.test.ts +137 -0
  136. package/tests/commands/domains/list.test.ts +164 -0
  137. package/tests/commands/domains/update.test.ts +223 -0
  138. package/tests/commands/domains/verify.test.ts +117 -0
  139. package/tests/commands/emails/batch.test.ts +313 -0
  140. package/tests/commands/emails/receiving/attachment.test.ts +140 -0
  141. package/tests/commands/emails/receiving/attachments.test.ts +168 -0
  142. package/tests/commands/emails/receiving/get.test.ts +140 -0
  143. package/tests/commands/emails/receiving/list.test.ts +181 -0
  144. package/tests/commands/emails/send.test.ts +309 -0
  145. package/tests/commands/segments/create.test.ts +163 -0
  146. package/tests/commands/segments/delete.test.ts +182 -0
  147. package/tests/commands/segments/get.test.ts +137 -0
  148. package/tests/commands/segments/list.test.ts +173 -0
  149. package/tests/commands/teams/list.test.ts +63 -0
  150. package/tests/commands/teams/remove.test.ts +103 -0
  151. package/tests/commands/teams/switch.test.ts +96 -0
  152. package/tests/commands/topics/create.test.ts +191 -0
  153. package/tests/commands/topics/delete.test.ts +156 -0
  154. package/tests/commands/topics/get.test.ts +125 -0
  155. package/tests/commands/topics/list.test.ts +124 -0
  156. package/tests/commands/topics/update.test.ts +177 -0
  157. package/tests/commands/webhooks/create.test.ts +224 -0
  158. package/tests/commands/webhooks/delete.test.ts +156 -0
  159. package/tests/commands/webhooks/get.test.ts +125 -0
  160. package/tests/commands/webhooks/list.test.ts +177 -0
  161. package/tests/commands/webhooks/update.test.ts +206 -0
  162. package/tests/commands/whoami.test.ts +99 -0
  163. package/tests/helpers.ts +93 -0
  164. package/tests/lib/client.test.ts +71 -0
  165. package/tests/lib/config.test.ts +414 -0
  166. package/tests/lib/files.test.ts +65 -0
  167. package/tests/lib/help-text.test.ts +96 -0
  168. package/tests/lib/output.test.ts +127 -0
  169. package/tests/lib/prompts.test.ts +178 -0
  170. package/tests/lib/spinner.test.ts +146 -0
  171. package/tests/lib/table.test.ts +63 -0
  172. package/tests/lib/tty.test.ts +85 -0
  173. package/tsconfig.json +14 -0
  174. package/src/index.js +0 -72
  175. package/src/routes.js +0 -37
  176. package/src/sections/apikeys.js +0 -99
  177. package/src/sections/audiences.js +0 -84
  178. package/src/sections/contacts.js +0 -177
  179. package/src/sections/domain.js +0 -195
  180. package/src/sections/email.js +0 -132
@@ -0,0 +1,14 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(git config:*)",
5
+ "Bash(find /Users/zeno/Projects/resend/resend-cli/src -name \"index.ts\" -path \"*/commands/index.ts\" -o -path \"*/cli.ts\" | xargs ls -la 2>/dev/null)",
6
+ "Bash(bun run:*)",
7
+ "Bash(./dist/resend --help 2>&1 | head -30)",
8
+ "Bash(./dist/resend whoami:*)",
9
+ "Bash(npx tsc:*)",
10
+ "Bash(npx vitest:*)",
11
+ "Bash(bun test:*)"
12
+ ]
13
+ }
14
+ }
@@ -0,0 +1,34 @@
1
+ import fs from 'node:fs';
2
+
3
+ const eventPath = process.env.GITHUB_EVENT_PATH;
4
+ if (!eventPath) {
5
+ console.error(
6
+ 'GITHUB_EVENT_PATH is not set. This script must run inside GitHub Actions.',
7
+ );
8
+ process.exit(1);
9
+ }
10
+
11
+ const eventJson = JSON.parse(fs.readFileSync(eventPath, 'utf8'));
12
+ const prTitle = eventJson.pull_request?.title;
13
+ if (!prTitle) {
14
+ console.error('Could not read pull_request.title from event payload.');
15
+ process.exit(1);
16
+ }
17
+
18
+ const isValidType = (title) =>
19
+ /^(feat|fix|chore|refactor)(\([a-zA-Z0-9-]+\))?:\s[a-z0-9].*$/.test(title);
20
+
21
+ const validateTitle = (title) => {
22
+ if (!isValidType(title)) {
23
+ console.error(
24
+ `PR title does not follow the required format "[type]: [description]"
25
+ Examples: "feat: add --cc flag" | "fix: domain fetch timeout"
26
+ Types: feat · fix · chore · refactor
27
+ Rules: lowercase after the colon`,
28
+ );
29
+ process.exit(1);
30
+ }
31
+ console.info(`PR title valid: "${title}"`);
32
+ };
33
+
34
+ validateTitle(prTitle);
@@ -0,0 +1,32 @@
1
+ name: CI
2
+ on:
3
+ push:
4
+ branches: [main]
5
+ pull_request:
6
+ concurrency:
7
+ group: ci-${{ github.ref }}
8
+ cancel-in-progress: true
9
+ jobs:
10
+ lint:
11
+ runs-on: blacksmith-2vcpu-ubuntu-2204
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: oven-sh/setup-bun@v2
15
+ - run: bun install --frozen-lockfile
16
+ - run: bun run lint
17
+
18
+ typecheck:
19
+ runs-on: blacksmith-2vcpu-ubuntu-2204
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+ - uses: oven-sh/setup-bun@v2
23
+ - run: bun install --frozen-lockfile
24
+ - run: bun run typecheck
25
+
26
+ test:
27
+ runs-on: blacksmith-2vcpu-ubuntu-2204
28
+ steps:
29
+ - uses: actions/checkout@v4
30
+ - uses: oven-sh/setup-bun@v2
31
+ - run: bun install --frozen-lockfile
32
+ - run: bun run test
@@ -0,0 +1,13 @@
1
+ name: PR Title Check
2
+ on:
3
+ pull_request:
4
+ types: [opened, edited, synchronize, reopened]
5
+ permissions:
6
+ contents: read
7
+ pull-requests: read
8
+ jobs:
9
+ pr-title-check:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - run: node .github/scripts/pr-title-check.js
@@ -0,0 +1,93 @@
1
+ name: Release
2
+ on:
3
+ push:
4
+ tags:
5
+ - 'v*'
6
+ permissions:
7
+ contents: write
8
+ concurrency:
9
+ group: release
10
+ cancel-in-progress: false
11
+ jobs:
12
+ release:
13
+ runs-on: blacksmith-2vcpu-ubuntu-2204
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - uses: oven-sh/setup-bun@v2
18
+
19
+ - name: Install dependencies
20
+ run: bun install --frozen-lockfile
21
+
22
+ - name: Build binaries
23
+ run: |
24
+ bun build --compile --minify --sourcemap --bytecode src/cli.ts --target=bun-darwin-arm64 --outfile dist/resend-darwin-arm64
25
+ bun build --compile --minify --sourcemap --bytecode src/cli.ts --target=bun-darwin-x64 --outfile dist/resend-darwin-x64
26
+ bun build --compile --minify --sourcemap --bytecode src/cli.ts --target=bun-linux-x64 --outfile dist/resend-linux-x64
27
+ bun build --compile --minify --sourcemap --bytecode src/cli.ts --target=bun-linux-arm64 --outfile dist/resend-linux-arm64
28
+ bun build --compile --minify --sourcemap --bytecode src/cli.ts --target=bun-windows-x64 --outfile dist/resend-windows-x64.exe
29
+
30
+ - name: Verify binaries
31
+ run: |
32
+ for f in dist/resend-darwin-arm64 dist/resend-darwin-x64 \
33
+ dist/resend-linux-x64 dist/resend-linux-arm64 \
34
+ dist/resend-windows-x64.exe; do
35
+ [ -s "$f" ] || { echo "Missing or empty: $f"; exit 1; }
36
+ done
37
+
38
+ - name: Package archives
39
+ run: |
40
+ cd dist
41
+ for bin in resend-darwin-arm64 resend-darwin-x64 resend-linux-x64 resend-linux-arm64; do
42
+ cp "$bin" resend
43
+ chmod +x resend
44
+ tar -czf "${bin}.tar.gz" resend
45
+ rm resend
46
+ done
47
+ cp resend-windows-x64.exe resend.exe
48
+ zip resend-windows-x64.zip resend.exe
49
+ rm resend.exe
50
+
51
+ - name: Create GitHub Release
52
+ uses: softprops/action-gh-release@v2
53
+ with:
54
+ generate_release_notes: true
55
+ body: |
56
+ ## Install
57
+
58
+ **macOS / Linux**
59
+ ```sh
60
+ curl -fsSL https://resend.com/install.sh | bash
61
+ ```
62
+
63
+ **Windows (PowerShell)**
64
+ ```pwsh
65
+ irm https://resend.com/install.ps1 | iex
66
+ ```
67
+
68
+ **GitHub CLI** _(use while repo is private)_
69
+ ```sh
70
+ gh release download --repo resend/resend-cli --pattern "resend-darwin-arm64.tar.gz"
71
+ tar -xzf resend-darwin-arm64.tar.gz && sudo mv resend /usr/local/bin/ && rm resend-darwin-arm64.tar.gz
72
+ ```
73
+ files: |
74
+ dist/resend-darwin-arm64.tar.gz
75
+ dist/resend-darwin-x64.tar.gz
76
+ dist/resend-linux-x64.tar.gz
77
+ dist/resend-linux-arm64.tar.gz
78
+ dist/resend-windows-x64.zip
79
+
80
+ notify-tap:
81
+ name: Trigger Homebrew Tap Update
82
+ needs: release
83
+ runs-on: ubuntu-latest
84
+ permissions:
85
+ contents: read
86
+ steps:
87
+ - name: Dispatch publish-release workflow on tap
88
+ env:
89
+ GH_TOKEN: ${{ secrets.RELEASE_CLI_ON_HOMEBREW }}
90
+ run: |
91
+ gh workflow run publish-release.yml \
92
+ --repo resend/homebrew-cli \
93
+ --field version="${GITHUB_REF_NAME}"
package/CHANGELOG.md ADDED
@@ -0,0 +1,31 @@
1
+ # Changelog
2
+
3
+ ## [0.2.0] - 2026-02-18
4
+
5
+ ### Added
6
+
7
+ - `resend domains` — create, verify, get, list, update, delete sending domains
8
+ - `resend api-keys` — create, list, delete API keys
9
+ - `resend broadcasts` — full broadcast lifecycle (create, send, get, list, update, delete)
10
+ - `resend contacts` — manage contacts, segments, and topics across all CRUD operations
11
+ - `resend emails batch` — send up to 100 emails in a single request from a JSON file
12
+ - Shared pagination (`--limit`, `--after`, `--before`) on all list commands
13
+ - `--html-file` flag on `emails send` and `broadcasts create` to read body from a file
14
+
15
+ ### Fixed
16
+
17
+ - `isInteractive()` now checks both `stdin` and `stdout` TTY — CI environments are correctly detected as non-interactive
18
+ - `domains delete` now returns a consistent `{ id, deleted: true }` object instead of an empty `{}`
19
+
20
+ ### Changed
21
+
22
+ - All delete commands return a uniform `{ object, id, deleted: true }` response
23
+ - `--help` improved across all commands with output shape, error codes, and usage examples
24
+
25
+ ---
26
+
27
+ ## [0.1.0] - 2026-02-18
28
+
29
+ - Initial release: `auth login`, `emails send`, `doctor`
30
+ - Auto JSON output when stdout is not a TTY (`--json`)
31
+ - Cross-platform binaries for macOS, Linux, and Windows via GitHub Actions
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2024 stretch
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Resend
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,19 +1,416 @@
1
- # resend-cli
2
- Simple, pretty command-line based tool for sending emails, managing audiences, etc via [Resend](https://resend.com).
3
-
4
- ## install
5
- Install resend-cli by running `npm i -g resend-cli`.
6
- Do not install resend-cli locally, without the `-g` flag, since it does not make sense for it to be a project dependency.
7
-
8
- ## usage
9
- resend-cli does not require any arguments. If you are running resend-cli for the first time, the command-line will prompt you for an API key.
10
- API keys must have full access for all features of the CLI to work properly. You may create an API key from the [dashboard](https://resend.com/api-keys).
11
-
12
- After this, the API key will be saved in plain text (!) at ~/.resend_config.json. For this reason, avoid installing resend-cli on public or shared computers.
13
-
14
- To exit resend-cli, use the Ctrl+C keyboard shortcut.
15
-
16
- ## misc
17
- visit the [project page](https://pablogracia.net/projects/resend-cli) to learn more about this project.
18
-
19
- Contributions welcome. Stars encouraged.
1
+ # Resend CLI
2
+
3
+ Command-line interface for the [Resend](https://resend.com) email API. Works for humans, AI agents, and CI/CD pipelines.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ curl -fsSL https://resend.com/install.sh | bash
9
+ ```
10
+
11
+ ## Local development
12
+
13
+ Use this when you want to change the CLI and run your build locally.
14
+
15
+ ### Prerequisites
16
+
17
+ - [Bun](https://bun.sh) (runtime and package manager)
18
+
19
+ ### Setup
20
+
21
+ 1. **Clone the repo**
22
+
23
+ ```bash
24
+ git clone https://github.com/resend/resend-cli.git
25
+ cd resend-cli
26
+ ```
27
+
28
+ 2. **Install dependencies**
29
+
30
+ ```bash
31
+ bun install
32
+ ```
33
+
34
+ 3. **Build locally**
35
+
36
+ ```bash
37
+ bun run build
38
+ ```
39
+
40
+ Output: `./dist/resend`
41
+
42
+ ### Running the CLI locally
43
+
44
+ Use the built binary directly:
45
+
46
+ ```bash
47
+ ./dist/resend --version
48
+ ```
49
+
50
+ Or add the project to your `PATH`:
51
+
52
+ ```bash
53
+ export PATH="$(pwd)/dist:$PATH"
54
+ resend doctor
55
+ ```
56
+
57
+ ### Making changes
58
+
59
+ After editing source files, rebuild:
60
+
61
+ ```bash
62
+ bun run build
63
+ ```
64
+
65
+ ## Quick start
66
+
67
+ ```bash
68
+ # Authenticate
69
+ resend login
70
+
71
+ # Send an email
72
+ resend emails send \
73
+ --from "you@yourdomain.com" \
74
+ --to recipient@example.com \
75
+ --subject "Hello from Resend CLI" \
76
+ --text "Sent from my terminal."
77
+
78
+ # Check your environment
79
+ resend doctor
80
+ ```
81
+
82
+ ---
83
+
84
+ ## Authentication
85
+
86
+ The CLI resolves your API key using the following priority chain:
87
+
88
+ | Priority | Source | How to set |
89
+ |----------|--------|------------|
90
+ | 1 (highest) | `--api-key` flag | `resend --api-key re_xxx emails send ...` |
91
+ | 2 | `RESEND_API_KEY` env var | `export RESEND_API_KEY=re_xxx` |
92
+ | 3 (lowest) | Config file | `resend login` |
93
+
94
+ If no key is found from any source, the CLI errors with code `auth_error`.
95
+
96
+ ---
97
+
98
+ ## Commands
99
+
100
+ ### `resend login`
101
+
102
+ Authenticate by storing your API key locally. The key is validated against the Resend API before being saved.
103
+
104
+ ```bash
105
+ resend login
106
+ ```
107
+
108
+ #### Interactive mode (default in terminals)
109
+
110
+ When run in a terminal, the command checks for an existing key:
111
+
112
+ - **No key found** — Offers to open the [Resend API keys dashboard](https://resend.com/api-keys) in your browser so you can create one, then prompts for the key.
113
+ - **Existing key found** — Shows the key source (`env`, `config`) and prompts for a new key to replace it.
114
+
115
+ The key is entered via a masked password input and must start with `re_`.
116
+
117
+ #### Non-interactive mode (CI, pipes, scripts)
118
+
119
+ When stdin is not a TTY, the `--key` flag is required:
120
+
121
+ ```bash
122
+ resend login --key re_xxxxxxxxxxxxx
123
+ ```
124
+
125
+ Omitting `--key` in non-interactive mode exits with error code `missing_key`.
126
+
127
+ #### Options
128
+
129
+ | Flag | Description |
130
+ |------|-------------|
131
+ | `--key <key>` | API key to store (required in non-interactive mode) |
132
+
133
+ #### Output
134
+
135
+ On success, credentials are saved to `~/.config/resend/credentials.json` with `0600` permissions (owner read/write only). The config directory is created with `0700` permissions.
136
+
137
+ ```bash
138
+ # JSON output
139
+ resend login --key re_xxx --json
140
+ # => {"success":true,"config_path":"/Users/you/.config/resend/credentials.json"}
141
+ ```
142
+
143
+ #### Error codes
144
+
145
+ | Code | Cause |
146
+ |------|-------|
147
+ | `missing_key` | No `--key` provided in non-interactive mode |
148
+ | `invalid_key_format` | Key does not start with `re_` |
149
+ | `validation_failed` | Resend API rejected the key |
150
+
151
+ ---
152
+
153
+ ### `resend emails send`
154
+
155
+ Send an email via the Resend API. Provide all options via flags for scripting, or let the CLI prompt interactively for missing fields.
156
+
157
+ ```bash
158
+ resend emails send \
159
+ --from "Name <sender@yourdomain.com>" \
160
+ --to recipient@example.com \
161
+ --subject "Subject line" \
162
+ --text "Plain text body"
163
+ ```
164
+
165
+ #### Options
166
+
167
+ | Flag | Required | Description |
168
+ |------|----------|-------------|
169
+ | `--from <address>` | Yes | Sender email address (must be from a verified domain) |
170
+ | `--to <addresses...>` | Yes | One or more recipient email addresses (space-separated) |
171
+ | `--subject <subject>` | Yes | Email subject line |
172
+ | `--text <text>` | One of text/html/html-file | Plain text body |
173
+ | `--html <html>` | One of text/html/html-file | HTML body as a string |
174
+ | `--html-file <path>` | One of text/html/html-file | Path to an HTML file to use as body |
175
+ | `--cc <addresses...>` | No | CC recipients (space-separated) |
176
+ | `--bcc <addresses...>` | No | BCC recipients (space-separated) |
177
+ | `--reply-to <address>` | No | Reply-to email address |
178
+
179
+ #### Interactive mode
180
+
181
+ When run in a terminal without all required flags, the CLI prompts for missing fields:
182
+
183
+ ```bash
184
+ # prompts for from, to, subject, and body
185
+ resend emails send
186
+
187
+ # prompts only for missing fields
188
+ resend emails send --from "you@yourdomain.com"
189
+ ```
190
+
191
+ #### Non-interactive mode
192
+
193
+ When piped or run in CI, all required flags must be provided. Missing flags cause an error listing what's needed:
194
+
195
+ ```bash
196
+ echo "" | resend emails send --from "you@yourdomain.com"
197
+ # Error: Missing required flags: --to, --subject
198
+ ```
199
+
200
+ A body (`--text`, `--html`, or `--html-file`) is also required — omitting all three exits with code `missing_body`.
201
+
202
+ #### Examples
203
+
204
+ **Multiple recipients:**
205
+
206
+ ```bash
207
+ resend emails send \
208
+ --from "you@yourdomain.com" \
209
+ --to alice@example.com bob@example.com \
210
+ --subject "Team update" \
211
+ --text "Hello everyone"
212
+ ```
213
+
214
+ **HTML from a file:**
215
+
216
+ ```bash
217
+ resend emails send \
218
+ --from "you@yourdomain.com" \
219
+ --to recipient@example.com \
220
+ --subject "Newsletter" \
221
+ --html-file ./newsletter.html
222
+ ```
223
+
224
+ **With CC, BCC, and reply-to:**
225
+
226
+ ```bash
227
+ resend emails send \
228
+ --from "you@yourdomain.com" \
229
+ --to recipient@example.com \
230
+ --subject "Meeting notes" \
231
+ --text "See attached." \
232
+ --cc manager@example.com \
233
+ --bcc archive@example.com \
234
+ --reply-to noreply@example.com
235
+ ```
236
+
237
+ **Overriding the API key for one send:**
238
+
239
+ ```bash
240
+ resend --api-key re_other_key emails send \
241
+ --from "you@yourdomain.com" \
242
+ --to recipient@example.com \
243
+ --subject "Test" \
244
+ --text "Using a different key"
245
+ ```
246
+
247
+ #### Output
248
+
249
+ Returns the email ID on success:
250
+
251
+ ```json
252
+ { "id": "49a3999c-0ce1-4ea6-ab68-afcd6dc2e794" }
253
+ ```
254
+
255
+ #### Error codes
256
+
257
+ | Code | Cause |
258
+ |------|-------|
259
+ | `auth_error` | No API key found or client creation failed |
260
+ | `missing_body` | No `--text`, `--html`, or `--html-file` provided |
261
+ | `file_read_error` | Could not read the file passed to `--html-file` |
262
+ | `send_error` | Resend API returned an error |
263
+
264
+ ---
265
+
266
+ ### `resend doctor`
267
+
268
+ Run environment diagnostics. Verifies your CLI version, API key, domains, and detects AI agent integrations.
269
+
270
+ ```bash
271
+ resend doctor
272
+ ```
273
+
274
+ #### Checks performed
275
+
276
+ | Check | Pass | Warn | Fail |
277
+ |-------|------|------|------|
278
+ | **CLI Version** | Running latest | Update available or registry unreachable | — |
279
+ | **API Key** | Key found (shows masked key + source) | — | No key found |
280
+ | **Domains** | Verified domains exist | No domains or all pending verification | API key invalid |
281
+ | **AI Agents** | Lists detected agents (or none) | — | — |
282
+
283
+ The API key is always masked in output (e.g. `re_...xxxx`).
284
+
285
+ #### Interactive mode
286
+
287
+ In a terminal, shows animated spinners for each check with colored status icons:
288
+
289
+ ```
290
+ Resend Doctor
291
+
292
+ ✔ CLI Version: v0.1.0 (latest)
293
+ ✔ API Key: re_...xxxx (source: env)
294
+ ✔ Domains: 2 verified, 0 pending
295
+ ✔ AI Agents: Detected: Cursor, Claude Desktop
296
+ ```
297
+
298
+ #### JSON mode
299
+
300
+ ```bash
301
+ resend doctor --json
302
+ ```
303
+
304
+ ```json
305
+ {
306
+ "ok": true,
307
+ "checks": [
308
+ { "name": "CLI Version", "status": "pass", "message": "v0.1.0 (latest)" },
309
+ { "name": "API Key", "status": "pass", "message": "re_...xxxx (source: env)" },
310
+ { "name": "Domains", "status": "pass", "message": "2 verified, 0 pending" },
311
+ { "name": "AI Agents", "status": "pass", "message": "Detected: Cursor" }
312
+ ]
313
+ }
314
+ ```
315
+
316
+ Each check has a `status` of `pass`, `warn`, or `fail`. The top-level `ok` is `false` if any check is `fail`.
317
+
318
+ #### Detected AI agents
319
+
320
+ | Agent | Detection method |
321
+ |-------|-----------------|
322
+ | OpenClaw | `~/clawd/skills` directory exists |
323
+ | Cursor | `~/.cursor` directory exists |
324
+ | Claude Desktop | Platform-specific config file exists |
325
+ | VS Code | `.vscode/mcp.json` in current directory |
326
+
327
+ #### Exit code
328
+
329
+ Exits `0` when all checks pass or warn. Exits `1` if any check fails.
330
+
331
+ ---
332
+
333
+ ## Global options
334
+
335
+ These flags work on every command and are passed before the subcommand:
336
+
337
+ ```bash
338
+ resend [global options] <command> [command options]
339
+ ```
340
+
341
+ | Flag | Description |
342
+ |------|-------------|
343
+ | `--api-key <key>` | Override API key for this invocation (takes highest priority) |
344
+ | `--json` | Force JSON output even in interactive terminals |
345
+ | `--version` | Print version and exit |
346
+ | `--help` | Show help text |
347
+
348
+ ---
349
+
350
+ ## Output behavior
351
+
352
+ The CLI has two output modes:
353
+
354
+ | Mode | When | Stdout | Stderr |
355
+ |------|------|--------|--------|
356
+ | **Interactive** | Terminal (TTY) | Formatted text | Spinners, prompts |
357
+ | **Machine** | Piped, CI, or `--json` | JSON | Nothing |
358
+
359
+ Switching is automatic — pipe to another command and JSON output activates:
360
+
361
+ ```bash
362
+ resend doctor | jq '.checks[].name'
363
+ resend emails send --from ... --to ... --subject ... --text ... | jq '.id'
364
+ ```
365
+
366
+ ### Error output
367
+
368
+ Errors always exit with code `1` and output structured JSON to stdout:
369
+
370
+ ```json
371
+ { "error": { "message": "No API key found", "code": "auth_error" } }
372
+ ```
373
+
374
+ ---
375
+
376
+ ## Agent & CI/CD usage
377
+
378
+ ### CI/CD
379
+
380
+ Set `RESEND_API_KEY` as an environment variable — no `resend login` needed:
381
+
382
+ ```yaml
383
+ # GitHub Actions
384
+ env:
385
+ RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
386
+ steps:
387
+ - run: |
388
+ resend emails send \
389
+ --from "deploy@yourdomain.com" \
390
+ --to "team@yourdomain.com" \
391
+ --subject "Deploy complete" \
392
+ --text "Version ${{ github.sha }} deployed."
393
+ ```
394
+
395
+ ### AI agents
396
+
397
+ Agents calling the CLI as a subprocess automatically get JSON output (non-TTY detection). The contract:
398
+
399
+ - **Input:** All required flags must be provided (no interactive prompts)
400
+ - **Output:** JSON to stdout, nothing to stderr
401
+ - **Exit code:** `0` success, `1` error
402
+ - **Errors:** Always include `message` and `code` fields
403
+
404
+ ---
405
+
406
+ ## Configuration
407
+
408
+ | Item | Path | Notes |
409
+ |------|------|-------|
410
+ | Config directory | `~/.config/resend/` | Respects `$XDG_CONFIG_HOME` on Linux, `%APPDATA%` on Windows |
411
+ | Credentials | `~/.config/resend/credentials.json` | `0600` permissions (owner read/write) |
412
+ | Install directory | `~/.resend/bin/` | Respects `$RESEND_INSTALL` |
413
+
414
+ ## License
415
+
416
+ MIT