resend-cli 1.0.3 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +5 -0
- package/.claude/worktrees/emails-list/.claude/settings.local.json +5 -0
- package/.claude/worktrees/emails-list/.github/scripts/pr-title-check.js +34 -0
- package/.claude/worktrees/emails-list/.github/workflows/ci.yml +32 -0
- package/.claude/worktrees/emails-list/.github/workflows/pr-title-check.yml +13 -0
- package/.claude/worktrees/emails-list/.github/workflows/release.yml +93 -0
- package/.claude/worktrees/emails-list/CHANGELOG.md +31 -0
- package/.claude/worktrees/emails-list/LICENSE +21 -0
- package/.claude/worktrees/emails-list/README.md +424 -0
- package/.claude/worktrees/emails-list/biome.json +36 -0
- package/.claude/worktrees/emails-list/bun.lock +76 -0
- package/.claude/worktrees/emails-list/bunfig.toml +2 -0
- package/.claude/worktrees/emails-list/install.ps1 +140 -0
- package/.claude/worktrees/emails-list/install.sh +301 -0
- package/.claude/worktrees/emails-list/package.json +43 -0
- package/.claude/worktrees/emails-list/renovate.json +6 -0
- package/.claude/worktrees/emails-list/src/cli.ts +74 -0
- package/.claude/worktrees/emails-list/src/commands/api-keys/create.ts +114 -0
- package/.claude/worktrees/emails-list/src/commands/api-keys/delete.ts +47 -0
- package/.claude/worktrees/emails-list/src/commands/api-keys/index.ts +26 -0
- package/.claude/worktrees/emails-list/src/commands/api-keys/list.ts +35 -0
- package/.claude/worktrees/emails-list/src/commands/api-keys/utils.ts +8 -0
- package/.claude/worktrees/emails-list/src/commands/auth/index.ts +20 -0
- package/.claude/worktrees/emails-list/src/commands/auth/login.ts +207 -0
- package/.claude/worktrees/emails-list/src/commands/auth/logout.ts +105 -0
- package/.claude/worktrees/emails-list/src/commands/broadcasts/create.ts +196 -0
- package/.claude/worktrees/emails-list/src/commands/broadcasts/delete.ts +46 -0
- package/.claude/worktrees/emails-list/src/commands/broadcasts/get.ts +59 -0
- package/.claude/worktrees/emails-list/src/commands/broadcasts/index.ts +43 -0
- package/.claude/worktrees/emails-list/src/commands/broadcasts/list.ts +60 -0
- package/.claude/worktrees/emails-list/src/commands/broadcasts/send.ts +56 -0
- package/.claude/worktrees/emails-list/src/commands/broadcasts/update.ts +95 -0
- package/.claude/worktrees/emails-list/src/commands/broadcasts/utils.ts +35 -0
- package/.claude/worktrees/emails-list/src/commands/contact-properties/create.ts +118 -0
- package/.claude/worktrees/emails-list/src/commands/contact-properties/delete.ts +48 -0
- package/.claude/worktrees/emails-list/src/commands/contact-properties/get.ts +46 -0
- package/.claude/worktrees/emails-list/src/commands/contact-properties/index.ts +48 -0
- package/.claude/worktrees/emails-list/src/commands/contact-properties/list.ts +68 -0
- package/.claude/worktrees/emails-list/src/commands/contact-properties/update.ts +88 -0
- package/.claude/worktrees/emails-list/src/commands/contact-properties/utils.ts +17 -0
- package/.claude/worktrees/emails-list/src/commands/contacts/add-segment.ts +78 -0
- package/.claude/worktrees/emails-list/src/commands/contacts/create.ts +122 -0
- package/.claude/worktrees/emails-list/src/commands/contacts/delete.ts +49 -0
- package/.claude/worktrees/emails-list/src/commands/contacts/get.ts +53 -0
- package/.claude/worktrees/emails-list/src/commands/contacts/index.ts +58 -0
- package/.claude/worktrees/emails-list/src/commands/contacts/list.ts +57 -0
- package/.claude/worktrees/emails-list/src/commands/contacts/remove-segment.ts +48 -0
- package/.claude/worktrees/emails-list/src/commands/contacts/segments.ts +39 -0
- package/.claude/worktrees/emails-list/src/commands/contacts/topics.ts +45 -0
- package/.claude/worktrees/emails-list/src/commands/contacts/update-topics.ts +90 -0
- package/.claude/worktrees/emails-list/src/commands/contacts/update.ts +77 -0
- package/.claude/worktrees/emails-list/src/commands/contacts/utils.ts +119 -0
- package/.claude/worktrees/emails-list/src/commands/doctor.ts +298 -0
- package/.claude/worktrees/emails-list/src/commands/domains/create.ts +83 -0
- package/.claude/worktrees/emails-list/src/commands/domains/delete.ts +42 -0
- package/.claude/worktrees/emails-list/src/commands/domains/get.ts +47 -0
- package/.claude/worktrees/emails-list/src/commands/domains/index.ts +35 -0
- package/.claude/worktrees/emails-list/src/commands/domains/list.ts +53 -0
- package/.claude/worktrees/emails-list/src/commands/domains/update.ts +75 -0
- package/.claude/worktrees/emails-list/src/commands/domains/utils.ts +44 -0
- package/.claude/worktrees/emails-list/src/commands/domains/verify.ts +38 -0
- package/.claude/worktrees/emails-list/src/commands/emails/batch.ts +140 -0
- package/.claude/worktrees/emails-list/src/commands/emails/index.ts +28 -0
- package/.claude/worktrees/emails-list/src/commands/emails/list.ts +73 -0
- package/.claude/worktrees/emails-list/src/commands/emails/receiving/attachment.ts +55 -0
- package/.claude/worktrees/emails-list/src/commands/emails/receiving/attachments.ts +68 -0
- package/.claude/worktrees/emails-list/src/commands/emails/receiving/get.ts +58 -0
- package/.claude/worktrees/emails-list/src/commands/emails/receiving/index.ts +28 -0
- package/.claude/worktrees/emails-list/src/commands/emails/receiving/list.ts +59 -0
- package/.claude/worktrees/emails-list/src/commands/emails/receiving/utils.ts +38 -0
- package/.claude/worktrees/emails-list/src/commands/emails/send.ts +189 -0
- package/.claude/worktrees/emails-list/src/commands/open.ts +24 -0
- package/.claude/worktrees/emails-list/src/commands/segments/create.ts +50 -0
- package/.claude/worktrees/emails-list/src/commands/segments/delete.ts +47 -0
- package/.claude/worktrees/emails-list/src/commands/segments/get.ts +38 -0
- package/.claude/worktrees/emails-list/src/commands/segments/index.ts +36 -0
- package/.claude/worktrees/emails-list/src/commands/segments/list.ts +58 -0
- package/.claude/worktrees/emails-list/src/commands/segments/utils.ts +7 -0
- package/.claude/worktrees/emails-list/src/commands/teams/index.ts +10 -0
- package/.claude/worktrees/emails-list/src/commands/teams/list.ts +35 -0
- package/.claude/worktrees/emails-list/src/commands/teams/remove.ts +83 -0
- package/.claude/worktrees/emails-list/src/commands/teams/switch.ts +73 -0
- package/.claude/worktrees/emails-list/src/commands/topics/create.ts +73 -0
- package/.claude/worktrees/emails-list/src/commands/topics/delete.ts +47 -0
- package/.claude/worktrees/emails-list/src/commands/topics/get.ts +42 -0
- package/.claude/worktrees/emails-list/src/commands/topics/index.ts +42 -0
- package/.claude/worktrees/emails-list/src/commands/topics/list.ts +34 -0
- package/.claude/worktrees/emails-list/src/commands/topics/update.ts +59 -0
- package/.claude/worktrees/emails-list/src/commands/topics/utils.ts +16 -0
- package/.claude/worktrees/emails-list/src/commands/webhooks/create.ts +128 -0
- package/.claude/worktrees/emails-list/src/commands/webhooks/delete.ts +49 -0
- package/.claude/worktrees/emails-list/src/commands/webhooks/get.ts +42 -0
- package/.claude/worktrees/emails-list/src/commands/webhooks/index.ts +44 -0
- package/.claude/worktrees/emails-list/src/commands/webhooks/list.ts +55 -0
- package/.claude/worktrees/emails-list/src/commands/webhooks/update.ts +83 -0
- package/.claude/worktrees/emails-list/src/commands/webhooks/utils.ts +36 -0
- package/.claude/worktrees/emails-list/src/commands/whoami.ts +71 -0
- package/.claude/worktrees/emails-list/src/lib/actions.ts +157 -0
- package/.claude/worktrees/emails-list/src/lib/client.ts +34 -0
- package/.claude/worktrees/emails-list/src/lib/config.ts +211 -0
- package/.claude/worktrees/emails-list/src/lib/files.ts +15 -0
- package/.claude/worktrees/emails-list/src/lib/help-text.ts +38 -0
- package/.claude/worktrees/emails-list/src/lib/output.ts +54 -0
- package/.claude/worktrees/emails-list/src/lib/pagination.ts +36 -0
- package/.claude/worktrees/emails-list/src/lib/prompts.ts +149 -0
- package/.claude/worktrees/emails-list/src/lib/spinner.ts +93 -0
- package/.claude/worktrees/emails-list/src/lib/table.ts +57 -0
- package/.claude/worktrees/emails-list/src/lib/tty.ts +28 -0
- package/.claude/worktrees/emails-list/src/lib/version.ts +4 -0
- package/.claude/worktrees/emails-list/tests/commands/api-keys/create.test.ts +195 -0
- package/.claude/worktrees/emails-list/tests/commands/api-keys/delete.test.ts +156 -0
- package/.claude/worktrees/emails-list/tests/commands/api-keys/list.test.ts +133 -0
- package/.claude/worktrees/emails-list/tests/commands/auth/login.test.ts +119 -0
- package/.claude/worktrees/emails-list/tests/commands/auth/logout.test.ts +146 -0
- package/.claude/worktrees/emails-list/tests/commands/broadcasts/create.test.ts +447 -0
- package/.claude/worktrees/emails-list/tests/commands/broadcasts/delete.test.ts +182 -0
- package/.claude/worktrees/emails-list/tests/commands/broadcasts/get.test.ts +146 -0
- package/.claude/worktrees/emails-list/tests/commands/broadcasts/list.test.ts +196 -0
- package/.claude/worktrees/emails-list/tests/commands/broadcasts/send.test.ts +161 -0
- package/.claude/worktrees/emails-list/tests/commands/broadcasts/update.test.ts +283 -0
- package/.claude/worktrees/emails-list/tests/commands/contact-properties/create.test.ts +250 -0
- package/.claude/worktrees/emails-list/tests/commands/contact-properties/delete.test.ts +183 -0
- package/.claude/worktrees/emails-list/tests/commands/contact-properties/get.test.ts +144 -0
- package/.claude/worktrees/emails-list/tests/commands/contact-properties/list.test.ts +180 -0
- package/.claude/worktrees/emails-list/tests/commands/contact-properties/update.test.ts +216 -0
- package/.claude/worktrees/emails-list/tests/commands/contacts/add-segment.test.ts +188 -0
- package/.claude/worktrees/emails-list/tests/commands/contacts/create.test.ts +270 -0
- package/.claude/worktrees/emails-list/tests/commands/contacts/delete.test.ts +192 -0
- package/.claude/worktrees/emails-list/tests/commands/contacts/get.test.ts +148 -0
- package/.claude/worktrees/emails-list/tests/commands/contacts/list.test.ts +175 -0
- package/.claude/worktrees/emails-list/tests/commands/contacts/remove-segment.test.ts +166 -0
- package/.claude/worktrees/emails-list/tests/commands/contacts/segments.test.ts +167 -0
- package/.claude/worktrees/emails-list/tests/commands/contacts/topics.test.ts +163 -0
- package/.claude/worktrees/emails-list/tests/commands/contacts/update-topics.test.ts +247 -0
- package/.claude/worktrees/emails-list/tests/commands/contacts/update.test.ts +205 -0
- package/.claude/worktrees/emails-list/tests/commands/doctor.test.ts +165 -0
- package/.claude/worktrees/emails-list/tests/commands/domains/create.test.ts +192 -0
- package/.claude/worktrees/emails-list/tests/commands/domains/delete.test.ts +156 -0
- package/.claude/worktrees/emails-list/tests/commands/domains/get.test.ts +137 -0
- package/.claude/worktrees/emails-list/tests/commands/domains/list.test.ts +164 -0
- package/.claude/worktrees/emails-list/tests/commands/domains/update.test.ts +223 -0
- package/.claude/worktrees/emails-list/tests/commands/domains/verify.test.ts +117 -0
- package/.claude/worktrees/emails-list/tests/commands/emails/batch.test.ts +313 -0
- package/.claude/worktrees/emails-list/tests/commands/emails/list.test.ts +196 -0
- package/.claude/worktrees/emails-list/tests/commands/emails/receiving/attachment.test.ts +140 -0
- package/.claude/worktrees/emails-list/tests/commands/emails/receiving/attachments.test.ts +168 -0
- package/.claude/worktrees/emails-list/tests/commands/emails/receiving/get.test.ts +140 -0
- package/.claude/worktrees/emails-list/tests/commands/emails/receiving/list.test.ts +181 -0
- package/.claude/worktrees/emails-list/tests/commands/emails/send.test.ts +309 -0
- package/.claude/worktrees/emails-list/tests/commands/segments/create.test.ts +163 -0
- package/.claude/worktrees/emails-list/tests/commands/segments/delete.test.ts +182 -0
- package/.claude/worktrees/emails-list/tests/commands/segments/get.test.ts +137 -0
- package/.claude/worktrees/emails-list/tests/commands/segments/list.test.ts +173 -0
- package/.claude/worktrees/emails-list/tests/commands/teams/list.test.ts +63 -0
- package/.claude/worktrees/emails-list/tests/commands/teams/remove.test.ts +103 -0
- package/.claude/worktrees/emails-list/tests/commands/teams/switch.test.ts +96 -0
- package/.claude/worktrees/emails-list/tests/commands/topics/create.test.ts +191 -0
- package/.claude/worktrees/emails-list/tests/commands/topics/delete.test.ts +156 -0
- package/.claude/worktrees/emails-list/tests/commands/topics/get.test.ts +125 -0
- package/.claude/worktrees/emails-list/tests/commands/topics/list.test.ts +124 -0
- package/.claude/worktrees/emails-list/tests/commands/topics/update.test.ts +177 -0
- package/.claude/worktrees/emails-list/tests/commands/webhooks/create.test.ts +224 -0
- package/.claude/worktrees/emails-list/tests/commands/webhooks/delete.test.ts +156 -0
- package/.claude/worktrees/emails-list/tests/commands/webhooks/get.test.ts +125 -0
- package/.claude/worktrees/emails-list/tests/commands/webhooks/list.test.ts +177 -0
- package/.claude/worktrees/emails-list/tests/commands/webhooks/update.test.ts +206 -0
- package/.claude/worktrees/emails-list/tests/commands/whoami.test.ts +99 -0
- package/.claude/worktrees/emails-list/tests/helpers.ts +93 -0
- package/.claude/worktrees/emails-list/tests/lib/client.test.ts +71 -0
- package/.claude/worktrees/emails-list/tests/lib/config.test.ts +414 -0
- package/.claude/worktrees/emails-list/tests/lib/files.test.ts +65 -0
- package/.claude/worktrees/emails-list/tests/lib/help-text.test.ts +97 -0
- package/.claude/worktrees/emails-list/tests/lib/output.test.ts +127 -0
- package/.claude/worktrees/emails-list/tests/lib/prompts.test.ts +178 -0
- package/.claude/worktrees/emails-list/tests/lib/spinner.test.ts +146 -0
- package/.claude/worktrees/emails-list/tests/lib/table.test.ts +63 -0
- package/.claude/worktrees/emails-list/tests/lib/tty.test.ts +85 -0
- package/.claude/worktrees/emails-list/tsconfig.json +14 -0
- package/.github/scripts/pr-title-check.js +34 -0
- package/.github/workflows/ci.yml +32 -0
- package/.github/workflows/pr-title-check.yml +13 -0
- package/.github/workflows/release.yml +93 -0
- package/.github/workflows/test-build-windows.yml +44 -0
- package/.github/workflows/test-install-windows.yml +48 -0
- package/CHANGELOG.md +31 -0
- package/LICENSE +21 -21
- package/README.md +424 -19
- package/biome.json +36 -0
- package/bun.lock +76 -0
- package/bunfig.toml +2 -0
- package/docs/agent-dx-gaps.md +167 -0
- package/docs/missing-commands.md +58 -0
- package/docs/production-readiness.md +99 -0
- package/docs/secure-key-storage.md +174 -0
- package/install.ps1 +141 -0
- package/install.sh +301 -0
- package/package.json +43 -22
- package/renovate.json +4 -0
- package/src/cli.ts +74 -0
- package/src/commands/api-keys/create.ts +114 -0
- package/src/commands/api-keys/delete.ts +47 -0
- package/src/commands/api-keys/index.ts +26 -0
- package/src/commands/api-keys/list.ts +35 -0
- package/src/commands/api-keys/utils.ts +8 -0
- package/src/commands/auth/index.ts +20 -0
- package/src/commands/auth/login.ts +232 -0
- package/src/commands/auth/logout.ts +105 -0
- package/src/commands/broadcasts/create.ts +196 -0
- package/src/commands/broadcasts/delete.ts +46 -0
- package/src/commands/broadcasts/get.ts +59 -0
- package/src/commands/broadcasts/index.ts +43 -0
- package/src/commands/broadcasts/list.ts +60 -0
- package/src/commands/broadcasts/send.ts +56 -0
- package/src/commands/broadcasts/update.ts +95 -0
- package/src/commands/broadcasts/utils.ts +35 -0
- package/src/commands/contact-properties/create.ts +118 -0
- package/src/commands/contact-properties/delete.ts +48 -0
- package/src/commands/contact-properties/get.ts +46 -0
- package/src/commands/contact-properties/index.ts +48 -0
- package/src/commands/contact-properties/list.ts +68 -0
- package/src/commands/contact-properties/update.ts +88 -0
- package/src/commands/contact-properties/utils.ts +17 -0
- package/src/commands/contacts/add-segment.ts +78 -0
- package/src/commands/contacts/create.ts +122 -0
- package/src/commands/contacts/delete.ts +49 -0
- package/src/commands/contacts/get.ts +53 -0
- package/src/commands/contacts/index.ts +58 -0
- package/src/commands/contacts/list.ts +57 -0
- package/src/commands/contacts/remove-segment.ts +48 -0
- package/src/commands/contacts/segments.ts +39 -0
- package/src/commands/contacts/topics.ts +45 -0
- package/src/commands/contacts/update-topics.ts +90 -0
- package/src/commands/contacts/update.ts +77 -0
- package/src/commands/contacts/utils.ts +119 -0
- package/src/commands/doctor.ts +298 -0
- package/src/commands/domains/create.ts +83 -0
- package/src/commands/domains/delete.ts +42 -0
- package/src/commands/domains/get.ts +47 -0
- package/src/commands/domains/index.ts +35 -0
- package/src/commands/domains/list.ts +53 -0
- package/src/commands/domains/update.ts +75 -0
- package/src/commands/domains/utils.ts +44 -0
- package/src/commands/domains/verify.ts +38 -0
- package/src/commands/emails/batch.ts +140 -0
- package/src/commands/emails/index.ts +24 -0
- package/src/commands/emails/receiving/attachment.ts +55 -0
- package/src/commands/emails/receiving/attachments.ts +68 -0
- package/src/commands/emails/receiving/get.ts +58 -0
- package/src/commands/emails/receiving/index.ts +28 -0
- package/src/commands/emails/receiving/list.ts +59 -0
- package/src/commands/emails/receiving/utils.ts +38 -0
- package/src/commands/emails/send.ts +189 -0
- package/src/commands/open.ts +24 -0
- package/src/commands/segments/create.ts +50 -0
- package/src/commands/segments/delete.ts +47 -0
- package/src/commands/segments/get.ts +38 -0
- package/src/commands/segments/index.ts +36 -0
- package/src/commands/segments/list.ts +58 -0
- package/src/commands/segments/utils.ts +7 -0
- package/src/commands/teams/index.ts +10 -0
- package/src/commands/teams/list.ts +35 -0
- package/src/commands/teams/remove.ts +86 -0
- package/src/commands/teams/switch.ts +76 -0
- package/src/commands/topics/create.ts +73 -0
- package/src/commands/topics/delete.ts +47 -0
- package/src/commands/topics/get.ts +42 -0
- package/src/commands/topics/index.ts +42 -0
- package/src/commands/topics/list.ts +34 -0
- package/src/commands/topics/update.ts +59 -0
- package/src/commands/topics/utils.ts +16 -0
- package/src/commands/webhooks/create.ts +128 -0
- package/src/commands/webhooks/delete.ts +49 -0
- package/src/commands/webhooks/get.ts +42 -0
- package/src/commands/webhooks/index.ts +44 -0
- package/src/commands/webhooks/list.ts +55 -0
- package/src/commands/webhooks/update.ts +83 -0
- package/src/commands/webhooks/utils.ts +36 -0
- package/src/commands/whoami.ts +71 -0
- package/src/lib/actions.ts +157 -0
- package/src/lib/client.ts +34 -0
- package/src/lib/config.ts +218 -0
- package/src/lib/files.ts +15 -0
- package/src/lib/help-text.ts +38 -0
- package/src/lib/output.ts +54 -0
- package/src/lib/pagination.ts +36 -0
- package/src/lib/prompts.ts +149 -0
- package/src/lib/spinner.ts +93 -0
- package/src/lib/table.ts +57 -0
- package/src/lib/tty.ts +28 -0
- package/src/lib/version.ts +4 -0
- package/tests/commands/api-keys/create.test.ts +195 -0
- package/tests/commands/api-keys/delete.test.ts +156 -0
- package/tests/commands/api-keys/list.test.ts +133 -0
- package/tests/commands/auth/login.test.ts +154 -0
- package/tests/commands/auth/logout.test.ts +146 -0
- package/tests/commands/broadcasts/create.test.ts +447 -0
- package/tests/commands/broadcasts/delete.test.ts +182 -0
- package/tests/commands/broadcasts/get.test.ts +146 -0
- package/tests/commands/broadcasts/list.test.ts +196 -0
- package/tests/commands/broadcasts/send.test.ts +161 -0
- package/tests/commands/broadcasts/update.test.ts +283 -0
- package/tests/commands/contact-properties/create.test.ts +250 -0
- package/tests/commands/contact-properties/delete.test.ts +183 -0
- package/tests/commands/contact-properties/get.test.ts +144 -0
- package/tests/commands/contact-properties/list.test.ts +180 -0
- package/tests/commands/contact-properties/update.test.ts +216 -0
- package/tests/commands/contacts/add-segment.test.ts +188 -0
- package/tests/commands/contacts/create.test.ts +270 -0
- package/tests/commands/contacts/delete.test.ts +192 -0
- package/tests/commands/contacts/get.test.ts +148 -0
- package/tests/commands/contacts/list.test.ts +175 -0
- package/tests/commands/contacts/remove-segment.test.ts +166 -0
- package/tests/commands/contacts/segments.test.ts +167 -0
- package/tests/commands/contacts/topics.test.ts +163 -0
- package/tests/commands/contacts/update-topics.test.ts +247 -0
- package/tests/commands/contacts/update.test.ts +205 -0
- package/tests/commands/doctor.test.ts +165 -0
- package/tests/commands/domains/create.test.ts +192 -0
- package/tests/commands/domains/delete.test.ts +156 -0
- package/tests/commands/domains/get.test.ts +137 -0
- package/tests/commands/domains/list.test.ts +164 -0
- package/tests/commands/domains/update.test.ts +223 -0
- package/tests/commands/domains/verify.test.ts +117 -0
- package/tests/commands/emails/batch.test.ts +313 -0
- package/tests/commands/emails/receiving/attachment.test.ts +140 -0
- package/tests/commands/emails/receiving/attachments.test.ts +168 -0
- package/tests/commands/emails/receiving/get.test.ts +140 -0
- package/tests/commands/emails/receiving/list.test.ts +181 -0
- package/tests/commands/emails/send.test.ts +309 -0
- package/tests/commands/segments/create.test.ts +163 -0
- package/tests/commands/segments/delete.test.ts +182 -0
- package/tests/commands/segments/get.test.ts +137 -0
- package/tests/commands/segments/list.test.ts +173 -0
- package/tests/commands/teams/list.test.ts +63 -0
- package/tests/commands/teams/remove.test.ts +103 -0
- package/tests/commands/teams/switch.test.ts +96 -0
- package/tests/commands/topics/create.test.ts +191 -0
- package/tests/commands/topics/delete.test.ts +156 -0
- package/tests/commands/topics/get.test.ts +125 -0
- package/tests/commands/topics/list.test.ts +124 -0
- package/tests/commands/topics/update.test.ts +177 -0
- package/tests/commands/webhooks/create.test.ts +224 -0
- package/tests/commands/webhooks/delete.test.ts +156 -0
- package/tests/commands/webhooks/get.test.ts +125 -0
- package/tests/commands/webhooks/list.test.ts +177 -0
- package/tests/commands/webhooks/update.test.ts +206 -0
- package/tests/commands/whoami.test.ts +99 -0
- package/tests/helpers.ts +93 -0
- package/tests/lib/client.test.ts +71 -0
- package/tests/lib/config.test.ts +447 -0
- package/tests/lib/files.test.ts +65 -0
- package/tests/lib/help-text.test.ts +97 -0
- package/tests/lib/output.test.ts +127 -0
- package/tests/lib/prompts.test.ts +178 -0
- package/tests/lib/spinner.test.ts +146 -0
- package/tests/lib/table.test.ts +63 -0
- package/tests/lib/tty.test.ts +85 -0
- package/tsconfig.json +14 -0
- package/src/index.js +0 -72
- package/src/routes.js +0 -37
- package/src/sections/apikeys.js +0 -99
- package/src/sections/audiences.js +0 -84
- package/src/sections/contacts.js +0 -177
- package/src/sections/domain.js +0 -195
- package/src/sections/email.js +0 -132
package/bun.lock
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 1,
|
|
3
|
+
"configVersion": 0,
|
|
4
|
+
"workspaces": {
|
|
5
|
+
"": {
|
|
6
|
+
"name": "resend-cli",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"@clack/prompts": "1.1.0",
|
|
9
|
+
"@commander-js/extra-typings": "14.0.0",
|
|
10
|
+
"commander": "14.0.3",
|
|
11
|
+
"resend": "6.9.3",
|
|
12
|
+
"unicode-animations": "1.0.3",
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@biomejs/biome": "2.4.6",
|
|
16
|
+
"@types/bun": "1.3.10",
|
|
17
|
+
"typescript": "5.9.3",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
"packages": {
|
|
22
|
+
"@biomejs/biome": ["@biomejs/biome@2.4.6", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.6", "@biomejs/cli-darwin-x64": "2.4.6", "@biomejs/cli-linux-arm64": "2.4.6", "@biomejs/cli-linux-arm64-musl": "2.4.6", "@biomejs/cli-linux-x64": "2.4.6", "@biomejs/cli-linux-x64-musl": "2.4.6", "@biomejs/cli-win32-arm64": "2.4.6", "@biomejs/cli-win32-x64": "2.4.6" }, "bin": { "biome": "bin/biome" } }, "sha512-QnHe81PMslpy3mnpL8DnO2M4S4ZnYPkjlGCLWBZT/3R9M6b5daArWMMtEfP52/n174RKnwRIf3oT8+wc9ihSfQ=="],
|
|
23
|
+
|
|
24
|
+
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NW18GSyxr+8sJIqgoGwVp5Zqm4SALH4b4gftIA0n62PTuBs6G2tHlwNAOj0Vq0KKSs7Sf88VjjmHh0O36EnzrQ=="],
|
|
25
|
+
|
|
26
|
+
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-4uiE/9tuI7cnjtY9b07RgS7gGyYOAfIAGeVJWEfeCnAarOAS7qVmuRyX6d7JTKw28/mt+rUzMasYeZ+0R/U1Mw=="],
|
|
27
|
+
|
|
28
|
+
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-kMLaI7OF5GN1Q8Doymjro1P8rVEoy7BKQALNz6fiR8IC1WKduoNyteBtJlHT7ASIL0Cx2jR6VUOBIbcB1B8pew=="],
|
|
29
|
+
|
|
30
|
+
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-F/JdB7eN22txiTqHM5KhIVt0jVkzZwVYrdTR1O3Y4auBOQcXxHK4dxULf4z43QyZI5tsnQJrRBHZy7wwtL+B3A=="],
|
|
31
|
+
|
|
32
|
+
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.6", "", { "os": "linux", "cpu": "x64" }, "sha512-oHXmUFEoH8Lql1xfc3QkFLiC1hGR7qedv5eKNlC185or+o4/4HiaU7vYODAH3peRCfsuLr1g6v2fK9dFFOYdyw=="],
|
|
33
|
+
|
|
34
|
+
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.6", "", { "os": "linux", "cpu": "x64" }, "sha512-C9s98IPDu7DYarjlZNuzJKTjVHN03RUnmHV5htvqsx6vEUXCDSJ59DNwjKVD5XYoSS4N+BYhq3RTBAL8X6svEg=="],
|
|
35
|
+
|
|
36
|
+
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-xzThn87Pf3YrOGTEODFGONmqXpTwUNxovQb72iaUOdcw8sBSY3+3WD8Hm9IhMYLnPi0n32s3L3NWU6+eSjfqFg=="],
|
|
37
|
+
|
|
38
|
+
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.6", "", { "os": "win32", "cpu": "x64" }, "sha512-7++XhnsPlr1HDbor5amovPjOH6vsrFOCdp93iKXhFn6bcMUI6soodj3WWKfgEO6JosKU1W5n3uky3WW9RlRjTg=="],
|
|
39
|
+
|
|
40
|
+
"@clack/core": ["@clack/core@1.1.0", "", { "dependencies": { "sisteransi": "^1.0.5" } }, "sha512-SVcm4Dqm2ukn64/8Gub2wnlA5nS2iWJyCkdNHcvNHPIeBTGojpdJ+9cZKwLfmqy7irD4N5qLteSilJlE0WLAtA=="],
|
|
41
|
+
|
|
42
|
+
"@clack/prompts": ["@clack/prompts@1.1.0", "", { "dependencies": { "@clack/core": "1.1.0", "sisteransi": "^1.0.5" } }, "sha512-pkqbPGtohJAvm4Dphs2M8xE29ggupihHdy1x84HNojZuMtFsHiUlRvqD24tM2+XmI+61LlfNceM3Wr7U5QES5g=="],
|
|
43
|
+
|
|
44
|
+
"@commander-js/extra-typings": ["@commander-js/extra-typings@14.0.0", "", { "peerDependencies": { "commander": "~14.0.0" } }, "sha512-hIn0ncNaJRLkZrxBIp5AsW/eXEHNKYQBh0aPdoUqNgD+Io3NIykQqpKFyKcuasZhicGaEZJX/JBSIkZ4e5x8Dg=="],
|
|
45
|
+
|
|
46
|
+
"@stablelib/base64": ["@stablelib/base64@1.0.1", "", {}, "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ=="],
|
|
47
|
+
|
|
48
|
+
"@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="],
|
|
49
|
+
|
|
50
|
+
"@types/node": ["@types/node@25.2.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ=="],
|
|
51
|
+
|
|
52
|
+
"bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="],
|
|
53
|
+
|
|
54
|
+
"commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
|
|
55
|
+
|
|
56
|
+
"fast-sha256": ["fast-sha256@1.3.0", "", {}, "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ=="],
|
|
57
|
+
|
|
58
|
+
"postal-mime": ["postal-mime@2.7.3", "", {}, "sha512-MjhXadAJaWgYzevi46+3kLak8y6gbg0ku14O1gO/LNOuay8dO+1PtcSGvAdgDR0DoIsSaiIA8y/Ddw6MnrO0Tw=="],
|
|
59
|
+
|
|
60
|
+
"resend": ["resend@6.9.3", "", { "dependencies": { "postal-mime": "2.7.3", "svix": "1.84.1" }, "peerDependencies": { "@react-email/render": "*" }, "optionalPeers": ["@react-email/render"] }, "sha512-GRXjH9XZBJA+daH7bBVDuTShr22iWCxXA8P7t495G4dM/RC+d+3gHBK/6bz9K6Vpcq11zRQKmD+B+jECwQlyGQ=="],
|
|
61
|
+
|
|
62
|
+
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
|
|
63
|
+
|
|
64
|
+
"standardwebhooks": ["standardwebhooks@1.0.0", "", { "dependencies": { "@stablelib/base64": "^1.0.0", "fast-sha256": "^1.3.0" } }, "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg=="],
|
|
65
|
+
|
|
66
|
+
"svix": ["svix@1.84.1", "", { "dependencies": { "standardwebhooks": "1.0.0", "uuid": "^10.0.0" } }, "sha512-K8DPPSZaW/XqXiz1kEyzSHYgmGLnhB43nQCMeKjWGCUpLIpAMMM8kx3rVVOSm6Bo6EHyK1RQLPT4R06skM/MlQ=="],
|
|
67
|
+
|
|
68
|
+
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
|
69
|
+
|
|
70
|
+
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
|
71
|
+
|
|
72
|
+
"unicode-animations": ["unicode-animations@1.0.3", "", { "dependencies": { "unicode-animations": "^1.0.1" }, "bin": { "unicode-animations": "scripts/demo.cjs" } }, "sha512-+klB2oWwcYZjYWhwP4Pr8UZffWDFVx6jKeIahE6z0QYyM2dwDeDPyn5nevCYbyotxvtT9lh21cVURO1RX0+YMg=="],
|
|
73
|
+
|
|
74
|
+
"uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="],
|
|
75
|
+
}
|
|
76
|
+
}
|
package/bunfig.toml
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Agent DX Gap Analysis
|
|
2
|
+
|
|
3
|
+
Gap analysis of the Resend CLI against the 7 dimensions from
|
|
4
|
+
["You Need to Rewrite Your CLI for AI Agents"](https://justinpoehnelt.com/rewrite-your-cli-for-agents/)
|
|
5
|
+
by Justin Poehnelt (Google).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Structured Output
|
|
10
|
+
|
|
11
|
+
> Agents need machine-readable output — JSON by default when piped.
|
|
12
|
+
|
|
13
|
+
### Strengths
|
|
14
|
+
|
|
15
|
+
- **Auto-JSON when piped.** `shouldOutputJson()` returns `true` when `!process.stdout.isTTY` (`src/lib/output.ts:14`). Every command goes through `outputResult` / `outputError`, so piped output is always JSON with zero flags required.
|
|
16
|
+
- **Consistent error envelope.** `outputError` emits `{"error":{"message":"…","code":"…"}}` with exit code 1 (`src/lib/output.ts:35-54`).
|
|
17
|
+
- **Help text documents the JSON shape.** `buildHelpText` includes an `output` section and `errorCodes` list (`src/lib/help-text.ts:1-34`).
|
|
18
|
+
|
|
19
|
+
### Gaps
|
|
20
|
+
|
|
21
|
+
| Gap | Details | Recommendation |
|
|
22
|
+
|-----|---------|----------------|
|
|
23
|
+
| No `--output` format flag | Only `--json` exists. No `--output=yaml`, `--output=csv`, or `--output=table`. | Low priority — JSON is sufficient for agents. Consider adding `--output` only if human table formatting is needed. |
|
|
24
|
+
| No JSON-Lines streaming | List endpoints return a single JSON blob. For large result sets an agent can't stream-process rows. | Add optional `--jsonl` or newline-delimited mode to `runList`. |
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 2. Deterministic, Non-Interactive Behavior
|
|
29
|
+
|
|
30
|
+
> When piped, CLIs must never prompt — they should fail fast with an actionable error.
|
|
31
|
+
|
|
32
|
+
### Strengths
|
|
33
|
+
|
|
34
|
+
- **TTY detection is solid.** `isInteractive()` checks `stdin.isTTY`, `stdout.isTTY`, `CI`, `GITHUB_ACTIONS`, and `TERM=dumb` (`src/lib/tty.ts:1-15`).
|
|
35
|
+
- **Missing flags → structured error.** `promptForMissing` exits with `missing_flags` code and lists which flags are needed (`src/lib/prompts.ts:55-61`).
|
|
36
|
+
- **Delete confirmation → structured error.** `confirmDelete` exits with `confirmation_required` and tells the agent to pass `--yes` (`src/lib/prompts.ts:28-36`).
|
|
37
|
+
- **Spinner is a no-op.** `createSpinner` returns stub methods in non-interactive mode (`src/lib/spinner.ts:48-55`).
|
|
38
|
+
|
|
39
|
+
### Gaps
|
|
40
|
+
|
|
41
|
+
| Gap | Details | Recommendation |
|
|
42
|
+
|-----|---------|----------------|
|
|
43
|
+
| `setup` command has no non-interactive default | Bare `resend setup` errors with `missing_target` when piped — the agent must already know the valid targets (`src/commands/setup/index.ts:70-79`). | Consider an `--all` flag or `resend setup --target cursor,vscode` to let agents configure multiple targets in one call. |
|
|
44
|
+
| `skills install` shells out to `npx skills` in TTY | Interactive path delegates to an external process (`src/commands/skills/install.ts:231-246`). If TTY detection is wrong, the agent gets an interactive subprocess. | The non-interactive path already exists and works — just ensure the interactive branch can never trigger when `--json` is passed (currently it can't, but worth a guard). |
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 3. Meaningful Exit Codes
|
|
49
|
+
|
|
50
|
+
> Agents rely on exit codes to branch logic. 0 = success, non-zero = specific failure category.
|
|
51
|
+
|
|
52
|
+
### Strengths
|
|
53
|
+
|
|
54
|
+
- **Errors exit 1.** `outputError` defaults to exit code 1 (`src/lib/output.ts:39`).
|
|
55
|
+
- **Error codes in JSON.** The `code` field (`missing_flags`, `auth_error`, `fetch_error`, etc.) lets agents distinguish failure types without parsing messages.
|
|
56
|
+
|
|
57
|
+
### Gaps
|
|
58
|
+
|
|
59
|
+
| Gap | Details | Recommendation |
|
|
60
|
+
|-----|---------|----------------|
|
|
61
|
+
| Single exit code for all errors | Everything exits 1. Auth failures, validation errors, network errors, and 404s are indistinguishable by exit code alone. | Define a small set of exit codes: `1` = general, `2` = usage/validation, `3` = auth, `4` = network/API, `78` = config. Map `outputError` codes to exit codes. |
|
|
62
|
+
| `process.exit(0)` on cancel | `cancelAndExit` exits 0 (`src/lib/prompts.ts:14-17`). A cancelled operation should exit non-zero so agents don't interpret it as success. | Exit 130 (standard SIGINT convention) or a dedicated code on cancellation. |
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 4. Stderr vs Stdout Separation
|
|
67
|
+
|
|
68
|
+
> Machines read stdout. Humans read stderr. Progress, spinners, and status go to stderr.
|
|
69
|
+
|
|
70
|
+
### Strengths
|
|
71
|
+
|
|
72
|
+
- **Spinners write to stderr.** All spinner output uses `process.stderr.write` (`src/lib/spinner.ts:62-80`).
|
|
73
|
+
- **Error JSON goes to stderr.** `outputError` uses `console.error` for JSON errors (`src/lib/output.ts:42`).
|
|
74
|
+
|
|
75
|
+
### Gaps
|
|
76
|
+
|
|
77
|
+
| Gap | Details | Recommendation |
|
|
78
|
+
|-----|---------|----------------|
|
|
79
|
+
| Interactive success messages go to stdout | `console.log(config.successMsg)` in `runDelete`, `runWrite`, and setup commands writes human text to stdout (`src/lib/actions.ts:67,124`). If an agent accidentally gets the interactive branch, it mixes human text with machine output. | Route all non-JSON human messages through `console.error` (stderr) so stdout is always machine-parseable. |
|
|
80
|
+
| `skills install` interactive output on stdout | `console.log(' ✔ ...')` in `installSkills` goes to stdout (`src/commands/skills/install.ts:173-178`). | Move to stderr. |
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 5. Discoverability & Self-Documentation
|
|
85
|
+
|
|
86
|
+
> Agents need to understand what a CLI can do from the CLI itself — not from docs websites.
|
|
87
|
+
|
|
88
|
+
### Strengths
|
|
89
|
+
|
|
90
|
+
- **Rich help text.** `buildHelpText` adds output shape, error codes, and examples to every command (`src/lib/help-text.ts`).
|
|
91
|
+
- **Examples are runnable.** Help text examples use real `$ resend ...` invocations.
|
|
92
|
+
- **MCP setup exists.** `resend setup` configures the CLI as an MCP server for 5 agents (`src/commands/setup/index.ts`).
|
|
93
|
+
|
|
94
|
+
### Gaps
|
|
95
|
+
|
|
96
|
+
| Gap | Details | Recommendation |
|
|
97
|
+
|-----|---------|----------------|
|
|
98
|
+
| No machine-readable command tree | `--help` outputs human-formatted text. There's no `resend --help --json` or `resend commands --json` that returns a structured list of all commands, flags, and types. | Add a hidden `resend commands` (or `--help --json`) that outputs the full command tree as JSON — subcommands, flags, types, defaults, required/optional. This is the single highest-impact improvement for agent discoverability. |
|
|
99
|
+
| No schema per command | Agents guess at flag types and constraints. | Emit JSON Schema for each command's input (flags + args) and output shape. Could be auto-generated from Commander metadata. |
|
|
100
|
+
| No version/capability negotiation | No `resend capabilities` or feature flags. An agent can't check if a subcommand exists without running it. | Add `resend capabilities --json` returning supported commands and API version. |
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## 6. Auth & Configuration
|
|
105
|
+
|
|
106
|
+
> Agents need non-interactive auth: env vars, config files, or flags — never browser OAuth flows.
|
|
107
|
+
|
|
108
|
+
### Strengths
|
|
109
|
+
|
|
110
|
+
- **Three-tier key resolution.** `resolveApiKey` checks `--api-key` flag → `RESEND_API_KEY` env → `~/.config/resend/credentials.json` (`src/lib/config.ts:24-45`). All three work without interaction.
|
|
111
|
+
- **Config file is secure.** Written with `0o600` permissions (`src/lib/config.ts:52-59`).
|
|
112
|
+
- **XDG-compliant.** Respects `XDG_CONFIG_HOME` and `APPDATA` (`src/lib/config.ts:14-22`).
|
|
113
|
+
|
|
114
|
+
### Gaps
|
|
115
|
+
|
|
116
|
+
| Gap | Details | Recommendation |
|
|
117
|
+
|-----|---------|----------------|
|
|
118
|
+
| No auth status command | There's no way to verify auth works without making an API call. | Add `resend auth status --json` → `{"authenticated":true,"source":"env","key_prefix":"re_..."}`. Useful for agents to pre-flight check credentials. |
|
|
119
|
+
| No key scoping info | The CLI doesn't surface whether the key is a full-access or sending-only key. | Return key metadata (permissions, team) from a status endpoint if the API supports it. |
|
|
120
|
+
| `resend login` is interactive-only | If `resend login` exists and requires TTY input, agents can't use it. | Ensure `resend login --api-key <key>` works non-interactively (just stores the key). |
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## 7. Idempotency & Safety
|
|
125
|
+
|
|
126
|
+
> Agent retries are inevitable. Commands should be safe to re-run.
|
|
127
|
+
|
|
128
|
+
### Strengths
|
|
129
|
+
|
|
130
|
+
- **Setup commands are idempotent.** `mergeJsonConfig` reads existing config and merges — running twice produces the same result (`src/commands/setup/utils.ts:14-33`). Help text documents this.
|
|
131
|
+
- **Skills install is idempotent.** Files are overwritten with the same content.
|
|
132
|
+
- **Delete requires `--yes`.** Prevents accidental destructive actions in non-interactive mode (`src/lib/prompts.ts:28-36`).
|
|
133
|
+
|
|
134
|
+
### Gaps
|
|
135
|
+
|
|
136
|
+
| Gap | Details | Recommendation |
|
|
137
|
+
|-----|---------|----------------|
|
|
138
|
+
| No idempotency keys for create operations | `resend emails send` or `resend domains create` with the same args may create duplicates. | Support `--idempotency-key <key>` passed through to the API's `Idempotency-Key` header. Critical for agent retry loops. |
|
|
139
|
+
| No dry-run mode | No `--dry-run` flag to preview what a command would do. | Add `--dry-run` that validates inputs, resolves auth, and returns the request that *would* be made — without executing it. |
|
|
140
|
+
| File reads have no path validation | `readFile` in `src/lib/files.ts` reads any path the process can access. An agent could be tricked into reading sensitive files. | Validate paths against an allowlist or working-directory scope. Low priority if the CLI is always invoked by a trusted agent. |
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Implementation Priority
|
|
145
|
+
|
|
146
|
+
Ordered by impact on agent usability:
|
|
147
|
+
|
|
148
|
+
| # | Item | Dimension | Effort |
|
|
149
|
+
|---|------|-----------|--------|
|
|
150
|
+
| 1 | `resend commands --json` — machine-readable command tree | Discoverability | Medium |
|
|
151
|
+
| 2 | Differentiated exit codes (2/3/4 by error category) | Exit Codes | Small |
|
|
152
|
+
| 3 | Route all human messages to stderr | Stderr/Stdout | Small |
|
|
153
|
+
| 4 | `resend auth status --json` | Auth | Small |
|
|
154
|
+
| 5 | `--idempotency-key` flag for create/send commands | Idempotency | Small |
|
|
155
|
+
| 6 | `--dry-run` flag | Idempotency | Medium |
|
|
156
|
+
| 7 | JSON Schema per command (input + output) | Discoverability | Large |
|
|
157
|
+
| 8 | `resend capabilities --json` | Discoverability | Medium |
|
|
158
|
+
| 9 | Cancel exits non-zero (130) | Exit Codes | Tiny |
|
|
159
|
+
| 10 | `--jsonl` streaming for list commands | Structured Output | Medium |
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Summary
|
|
164
|
+
|
|
165
|
+
The Resend CLI already handles the two most common agent pitfalls well: **structured output is automatic when piped**, and **interactive prompts fail fast with actionable errors**. The spinner, auth, and setup systems are agent-friendly out of the box.
|
|
166
|
+
|
|
167
|
+
The biggest gaps are in **discoverability** (no way for an agent to introspect available commands as JSON) and **exit code granularity** (everything is exit 1). Fixing items 1-4 above would cover ~80% of the agent DX surface with relatively small changes.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Missing CLI Commands
|
|
2
|
+
|
|
3
|
+
Commands supported by the SDK (`resend` npm package) that have no CLI equivalent yet.
|
|
4
|
+
|
|
5
|
+
Source comparison: `resend-node/src/` resource classes vs `resend-cli/src/commands/`.
|
|
6
|
+
|
|
7
|
+
## Templates (7 commands)
|
|
8
|
+
|
|
9
|
+
The entire `templates` namespace is missing from the CLI.
|
|
10
|
+
|
|
11
|
+
| Command | SDK method | Notes |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `resend templates create` | `templates.create(payload)` | `--name`, `--subject`, `--html`, `--text` |
|
|
14
|
+
| `resend templates list` | `templates.list(opts)` | Pagination support |
|
|
15
|
+
| `resend templates get <id>` | `templates.get(id)` | Also supports alias lookup |
|
|
16
|
+
| `resend templates update <id>` | `templates.update(id, payload)` | `--name`, `--subject`, `--html`, `--text` |
|
|
17
|
+
| `resend templates delete <id>` | `templates.remove(id)` | Confirmation prompt |
|
|
18
|
+
| `resend templates duplicate <id>` | `templates.duplicate(id)` | |
|
|
19
|
+
| `resend templates publish <id>` | `templates.publish(id)` | |
|
|
20
|
+
|
|
21
|
+
## Sent Emails (4 commands)
|
|
22
|
+
|
|
23
|
+
The `emails` namespace only has `send` and `batch`. These read/manage operations are missing:
|
|
24
|
+
|
|
25
|
+
| Command | SDK method | Notes |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| `resend emails list` | `emails.list(opts)` | Pagination support |
|
|
28
|
+
| `resend emails get <id>` | `emails.get(id)` | Retrieve a sent email by ID |
|
|
29
|
+
| `resend emails update <id>` | `emails.update(payload)` | Reschedule a scheduled email (`--scheduled-at`) |
|
|
30
|
+
| `resend emails cancel <id>` | `emails.cancel(id)` | Cancel a scheduled email |
|
|
31
|
+
|
|
32
|
+
## Not planned
|
|
33
|
+
|
|
34
|
+
These SDK methods don't map cleanly to CLI commands:
|
|
35
|
+
|
|
36
|
+
| SDK method | Reason |
|
|
37
|
+
|---|---|
|
|
38
|
+
| `emails.receiving.forward(opts)` | Convenience wrapper that calls `emails.send` internally, not a distinct API endpoint |
|
|
39
|
+
| `webhooks.verify(payload)` | Signature verification utility using Svix. Used in server-side webhook handlers, not useful as a CLI command |
|
|
40
|
+
|
|
41
|
+
## Coverage summary
|
|
42
|
+
|
|
43
|
+
| Namespace | SDK methods | CLI commands | Coverage |
|
|
44
|
+
|---|---|---|---|
|
|
45
|
+
| emails (send/batch) | 6 | 2 | 33% |
|
|
46
|
+
| emails.receiving | 5 | 4 | 80% |
|
|
47
|
+
| domains | 6 | 6 | 100% |
|
|
48
|
+
| api-keys | 3 | 3 | 100% |
|
|
49
|
+
| contacts | 5 | 5 | 100% |
|
|
50
|
+
| contacts.segments | 3 | 3 | 100% |
|
|
51
|
+
| contacts.topics | 2 | 2 | 100% |
|
|
52
|
+
| broadcasts | 6 | 6 | 100% |
|
|
53
|
+
| contact-properties | 5 | 5 | 100% |
|
|
54
|
+
| segments | 4 | 4 | 100% |
|
|
55
|
+
| topics | 5 | 5 | 100% |
|
|
56
|
+
| templates | 7 | 0 | 0% |
|
|
57
|
+
| webhooks | 5 | 5 | 100% |
|
|
58
|
+
| **Total** | **62** | **51** | **82%** |
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Production Readiness Gaps
|
|
2
|
+
|
|
3
|
+
Tracked gaps identified during full CLI review. Items are ordered by severity.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Blockers
|
|
8
|
+
|
|
9
|
+
### `removeApiKey()` crashes when no credentials file exists
|
|
10
|
+
|
|
11
|
+
`src/lib/config.ts` — `unlinkSync()` called without try-catch. Running `resend auth logout` when not logged in throws an unhandled exception instead of a graceful error.
|
|
12
|
+
|
|
13
|
+
### `emails send` missing attachment support
|
|
14
|
+
|
|
15
|
+
The Resend API supports `attachments` on send but the CLI does not expose an `--attach <path>` flag. This is a core email feature users will expect before adopting the CLI.
|
|
16
|
+
|
|
17
|
+
### `emails send` missing `--scheduled-at`
|
|
18
|
+
|
|
19
|
+
The API supports scheduled sending and broadcasts already expose `--scheduled-at`, but single email send does not.
|
|
20
|
+
|
|
21
|
+
### Distribution: Homebrew tap and additional package managers
|
|
22
|
+
|
|
23
|
+
The release pipeline builds binaries and publishes GitHub Releases, but there is no official Homebrew tap under the `resend` org. The previous reference to a personal tap (`rafa-thayto/homebrew-tap`) was removed. Before launch, set up:
|
|
24
|
+
|
|
25
|
+
- **Homebrew** — Create `resend/homebrew-tap` with a formula that downloads the correct binary from GitHub Releases. Wire the release workflow to auto-update the formula on new tags.
|
|
26
|
+
- **npm** — The package defines a `bin` entry but is not currently published to npm. `npm install -g @resend/cli` would be the expected install path for Node.js users.
|
|
27
|
+
- **AUR / Scoop / winget** — Consider community packages for Linux and Windows users who don't use the shell installer.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## High Priority
|
|
32
|
+
|
|
33
|
+
### No shell completions
|
|
34
|
+
|
|
35
|
+
No bash, zsh, or fish completion support exists. With 50+ commands and deeply nested subcommands, tab completion is essential for discoverability and usability.
|
|
36
|
+
|
|
37
|
+
### Inconsistent pagination on list commands
|
|
38
|
+
|
|
39
|
+
`api-keys list` and `topics list` lack the `--limit`, `--after`, and `--before` options that every other list command provides. This creates an inconsistent scripting surface.
|
|
40
|
+
|
|
41
|
+
**Files:** `src/commands/api-keys/list.ts`, `src/commands/topics/list.ts`
|
|
42
|
+
|
|
43
|
+
### No tests for `actions.ts` and `pagination.ts`
|
|
44
|
+
|
|
45
|
+
These are core shared modules (reusable action builders and pagination logic) used by nearly every command, yet they have zero test coverage.
|
|
46
|
+
|
|
47
|
+
**Files:** `src/lib/actions.ts`, `src/lib/pagination.ts`
|
|
48
|
+
|
|
49
|
+
### Batch >100 emails warns but does not error in non-interactive mode
|
|
50
|
+
|
|
51
|
+
`src/commands/emails/batch.ts` uses `console.warn()` when the batch exceeds 100 emails but continues execution. In CI or scripted mode this should hard-error since the API will reject the request.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Medium Priority
|
|
56
|
+
|
|
57
|
+
### No output format options beyond JSON and table
|
|
58
|
+
|
|
59
|
+
List commands support `--json` or interactive tables only. Missing `--format csv` and `--format tsv` for data export workflows.
|
|
60
|
+
|
|
61
|
+
### No filter or search on list commands
|
|
62
|
+
|
|
63
|
+
No ability to filter results server-side (e.g. `--status verified` on domains, `--segment-id` on contacts). Users must fetch all pages and filter locally.
|
|
64
|
+
|
|
65
|
+
### No `resend update` or auto-update mechanism
|
|
66
|
+
|
|
67
|
+
Version check exists in `resend doctor` but there is no way to update the CLI from within it. Users must re-run the install script manually.
|
|
68
|
+
|
|
69
|
+
### `setup --uninstall` and `setup --dry-run` missing
|
|
70
|
+
|
|
71
|
+
The setup command can configure AI agent integrations but cannot undo or preview changes before applying them.
|
|
72
|
+
|
|
73
|
+
### No CLI-level retry logic for transient failures
|
|
74
|
+
|
|
75
|
+
The CLI relies entirely on the SDK's internal retries which are opaque. For batch and bulk operations, CLI-level retry with exponential backoff would be more robust and give users visibility into retries.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Low Priority
|
|
80
|
+
|
|
81
|
+
### Table rendering does not truncate long values
|
|
82
|
+
|
|
83
|
+
Very long emails, domain names, or property values can break table alignment in narrow terminals.
|
|
84
|
+
|
|
85
|
+
### No `--silent` or `--quiet` flag
|
|
86
|
+
|
|
87
|
+
No way to suppress all non-essential output (spinners, hints, pagination messages) for scripting use cases.
|
|
88
|
+
|
|
89
|
+
### Contact email-vs-ID detection is fragile
|
|
90
|
+
|
|
91
|
+
`src/commands/contacts/utils.ts` uses `includes('@')` to decide whether a string is an email address or a contact ID. Edge cases could cause misidentification.
|
|
92
|
+
|
|
93
|
+
### No client-side input validation for domains and emails
|
|
94
|
+
|
|
95
|
+
Domain names and email addresses are accepted as-is and forwarded to the API. Client-side validation would provide faster, clearer error messages.
|
|
96
|
+
|
|
97
|
+
### Hardcoded API URLs prevent staging usage
|
|
98
|
+
|
|
99
|
+
No `--base-url` flag or `RESEND_BASE_URL` environment variable for pointing the CLI at non-production environments.
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Secure API Key Storage via macOS Keychain
|
|
2
|
+
|
|
3
|
+
## Context
|
|
4
|
+
|
|
5
|
+
API keys are currently stored as **plain text** in `~/.config/resend/credentials.json` (with `0o600` file permissions). The CLI already supports **multi-team profiles** — the credentials file has this structure:
|
|
6
|
+
|
|
7
|
+
```json
|
|
8
|
+
{ "active_team": "production", "teams": { "production": { "api_key": "re_..." }, "staging": { "api_key": "re_..." } } }
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The keychain integration must preserve this multi-team model: each team's API key gets its own keychain entry, and team metadata (active team, team list) stays in the config file.
|
|
12
|
+
|
|
13
|
+
**Goal:** Move API key secrets into macOS Keychain while keeping team management metadata in the config file.
|
|
14
|
+
|
|
15
|
+
## Approach: Shell out to `/usr/bin/security`
|
|
16
|
+
|
|
17
|
+
Use macOS's built-in `security` CLI to interact with Keychain. Zero dependencies, works with Bun compiled binaries.
|
|
18
|
+
|
|
19
|
+
Each team gets its own keychain entry using the team name as the `account` field:
|
|
20
|
+
- `security add-generic-password -s resend-cli -a <team> -w <key> -U` (store/update)
|
|
21
|
+
- `security find-generic-password -s resend-cli -a <team> -w` (retrieve)
|
|
22
|
+
- `security delete-generic-password -s resend-cli -a <team>` (delete)
|
|
23
|
+
|
|
24
|
+
**Why not keytar (the prototype's approach):**
|
|
25
|
+
`bun build --compile` produces a self-contained binary with embedded JS bytecode. Native N-API addons (`.node` files) cannot be embedded — they require OS-level dynamic library loading at runtime. The release workflow (`.github/workflows/release.yml`) cross-compiles to 5 platform targets from a single Ubuntu runner, which further rules out native modules needing per-platform node-gyp compilation. The prototype uses keytar because it's a standard Node.js/oclif package where native modules compile during `npm install`.
|
|
26
|
+
|
|
27
|
+
**Why not other approaches:**
|
|
28
|
+
- **Bun FFI**: Hundreds of lines of C-interop for CoreFoundation/Security.framework — not worth it
|
|
29
|
+
- **Pure-JS wrappers**: Most just shell out to `security` internally anyway
|
|
30
|
+
|
|
31
|
+
## Key resolution priority (updated)
|
|
32
|
+
|
|
33
|
+
1. `--api-key` flag → source: `'flag'`
|
|
34
|
+
2. `RESEND_API_KEY` env var → source: `'env'`
|
|
35
|
+
3. **macOS Keychain** (for resolved team name) → source: `'keychain'` *(new)*
|
|
36
|
+
4. `credentials.json` teams → source: `'config'` *(kept as fallback)*
|
|
37
|
+
|
|
38
|
+
Team resolution is unchanged: `--team` flag > `RESEND_TEAM` env > `active_team` in config > `"default"`
|
|
39
|
+
|
|
40
|
+
## Storage model
|
|
41
|
+
|
|
42
|
+
**Keychain** stores secrets only:
|
|
43
|
+
- Service: `resend-cli`, Account: `<team-name>`, Password: `<api-key>`
|
|
44
|
+
- One entry per team
|
|
45
|
+
|
|
46
|
+
**Config file** (`credentials.json`) stores non-secret metadata:
|
|
47
|
+
- `active_team`: which team is currently active
|
|
48
|
+
- `teams`: record of team names (but **no more `api_key` fields** when keychain is available)
|
|
49
|
+
|
|
50
|
+
When keychain is available, the config file looks like:
|
|
51
|
+
```json
|
|
52
|
+
{ "active_team": "production", "teams": { "production": {}, "staging": {} } }
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
When keychain is unavailable (fallback), `api_key` is stored in the teams record as before.
|
|
56
|
+
|
|
57
|
+
## Migration strategy
|
|
58
|
+
|
|
59
|
+
No automatic migration. Both backends coexist:
|
|
60
|
+
- New `resend login` writes key to keychain + team metadata to config (no `api_key` in config)
|
|
61
|
+
- Old config files with `api_key` fields still work at lower priority
|
|
62
|
+
- `resend logout` / `resend teams remove` clean up both keychain entry and config
|
|
63
|
+
- Users naturally migrate when they next run `resend login`
|
|
64
|
+
|
|
65
|
+
## Files to change
|
|
66
|
+
|
|
67
|
+
### New: `src/lib/keychain.ts` (~60 lines)
|
|
68
|
+
|
|
69
|
+
Keychain abstraction wrapping `/usr/bin/security` via `child_process.execFile` (arg array, no shell — safe from injection).
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
keychainStore(team: string, apiKey: string): Promise<void>
|
|
73
|
+
keychainGet(team: string): Promise<string | null> // null on not-found (exit code 44)
|
|
74
|
+
keychainDelete(team: string): Promise<boolean>
|
|
75
|
+
keychainAvailable(): boolean // process.platform === 'darwin' && existsSync('/usr/bin/security')
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Constants: `SERVICE = 'resend-cli'`
|
|
79
|
+
|
|
80
|
+
### Modify: `src/lib/config.ts`
|
|
81
|
+
|
|
82
|
+
Current functions and how they change:
|
|
83
|
+
|
|
84
|
+
- `ApiKeySource` type: add `'keychain'`
|
|
85
|
+
- `TeamProfile` type: make `api_key` optional (`{ api_key?: string }`) — empty when key is in keychain
|
|
86
|
+
- `resolveApiKey(flagValue?, teamName?)` → **async**: after env check, try `keychainGet(team)` before falling back to config file's `api_key`
|
|
87
|
+
- `storeApiKey(apiKey, teamName?)` → **async**: if `keychainAvailable()`, store key in keychain and write team to config without `api_key`; otherwise write `api_key` to config as fallback
|
|
88
|
+
- `removeApiKey(teamName?)` → **async**: delete from keychain + remove team from config
|
|
89
|
+
- `removeTeam(teamName)` → **async**: delete from keychain + existing config cleanup
|
|
90
|
+
- `resolveTeamName`, `readCredentials`, `writeCredentials`, `setActiveTeam`, `listTeams`: unchanged (they don't touch API keys directly)
|
|
91
|
+
|
|
92
|
+
### Modify: `src/lib/client.ts`
|
|
93
|
+
|
|
94
|
+
- `createClient` and `requireClient` → **async** (add `await` to `resolveApiKey`)
|
|
95
|
+
|
|
96
|
+
### Modify: `src/commands/auth/login.ts`
|
|
97
|
+
|
|
98
|
+
- `await storeApiKey(apiKey, teamName)` (line 151)
|
|
99
|
+
- Update success message: "API key stored in macOS Keychain" vs "API key stored at {path}"
|
|
100
|
+
- `await resolveApiKey()` (line 71)
|
|
101
|
+
|
|
102
|
+
### Modify: `src/commands/auth/logout.ts`
|
|
103
|
+
|
|
104
|
+
- Check keychain + config to determine if logged in (not just file existence)
|
|
105
|
+
- `await removeApiKey()`
|
|
106
|
+
- Update confirmation message for keychain
|
|
107
|
+
|
|
108
|
+
### Modify: `src/commands/teams/remove.ts`
|
|
109
|
+
|
|
110
|
+
- `await removeTeam(name)` — needs to delete keychain entry too
|
|
111
|
+
|
|
112
|
+
### Modify: `src/commands/doctor.ts`
|
|
113
|
+
|
|
114
|
+
- `checkApiKeyPresence` → **async**: `await resolveApiKey()`
|
|
115
|
+
- `checkApiValidationAndDomains` → `await resolveApiKey()`
|
|
116
|
+
- Show `(source: keychain)` when applicable
|
|
117
|
+
- When source is `'config'`, hint: "run `resend login` to migrate to Keychain"
|
|
118
|
+
|
|
119
|
+
### Modify: `src/commands/setup/utils.ts` and `src/commands/setup/claude-code.ts`
|
|
120
|
+
|
|
121
|
+
- Add `await` to `resolveApiKey()` calls
|
|
122
|
+
|
|
123
|
+
### Update help text in: `src/cli.ts`, `login.ts`, `logout.ts`
|
|
124
|
+
|
|
125
|
+
- Reflect new priority chain mentioning Keychain
|
|
126
|
+
|
|
127
|
+
### New: `tests/lib/keychain.test.ts`
|
|
128
|
+
|
|
129
|
+
- Mock `execFile` to test store/get/delete without touching real keychain
|
|
130
|
+
- Test per-team accounts work correctly
|
|
131
|
+
- Test `keychainGet` returns `null` on exit code 44
|
|
132
|
+
- Test `keychainAvailable` returns `false` on non-darwin
|
|
133
|
+
|
|
134
|
+
### Update: existing tests
|
|
135
|
+
|
|
136
|
+
- Mock keychain module in config/auth tests
|
|
137
|
+
|
|
138
|
+
## File-based fallback hardening
|
|
139
|
+
|
|
140
|
+
When keychain is unavailable (headless Linux, CI, containers), the file-based backend is used with hardening:
|
|
141
|
+
|
|
142
|
+
1. **Warn on fallback**: When `storeApiKey` writes to file instead of keychain, print:
|
|
143
|
+
`"⚠ No system keychain available. API key stored in plain text at {path}. Consider using RESEND_API_KEY env var instead."`
|
|
144
|
+
|
|
145
|
+
2. **Permission check on read**: When `resolveApiKey` reads from the config file, verify permissions are still `0o600`. If loosened, warn:
|
|
146
|
+
`"⚠ Credentials file {path} has loose permissions. Run: chmod 600 {path}"`
|
|
147
|
+
|
|
148
|
+
3. **Doctor check**: Show storage backend and warn if using file-based storage on a platform that supports keychain.
|
|
149
|
+
|
|
150
|
+
**Industry context**: Plain text storage is common (AWS CLI, Stripe CLI, Heroku) but increasingly criticized. GitHub CLI now supports keychain (becoming default). Google Cloud uses encrypted SQLite. The trend is toward OS-level secure storage. The file fallback exists for environments where no keychain daemon is available.
|
|
151
|
+
|
|
152
|
+
## Future: Linux & Windows support
|
|
153
|
+
|
|
154
|
+
The `keychain.ts` module abstracts the backend per platform:
|
|
155
|
+
|
|
156
|
+
| Platform | System | CLI tool |
|
|
157
|
+
|----------|--------|----------|
|
|
158
|
+
| **macOS** | Keychain Access | `/usr/bin/security` |
|
|
159
|
+
| **Linux** | libsecret / GNOME Keyring | `secret-tool` (`libsecret-tools`) |
|
|
160
|
+
| **Windows** | Credential Manager | `cmdkey` or PowerShell |
|
|
161
|
+
|
|
162
|
+
To add a platform: add platform-specific functions in `keychain.ts` and extend `keychainAvailable()`. The file-based fallback always remains for headless/CI environments.
|
|
163
|
+
|
|
164
|
+
## Verification
|
|
165
|
+
|
|
166
|
+
1. `bun run build` — ensure compiled binary works
|
|
167
|
+
2. `resend login --key re_test` — stores in keychain, verify in Keychain Access.app under service "resend-cli"
|
|
168
|
+
3. `resend login --key re_staging` with `--team staging` — second keychain entry
|
|
169
|
+
4. `resend teams list` — shows both teams
|
|
170
|
+
5. `resend teams switch staging` → `resend doctor` — shows staging key from keychain
|
|
171
|
+
6. `resend teams remove staging` — deletes keychain entry
|
|
172
|
+
7. `resend logout` — removes active team's keychain entry
|
|
173
|
+
8. `RESEND_API_KEY=re_env resend doctor` — env override still works
|
|
174
|
+
9. Test with old `credentials.json` containing `api_key` fields — falls back to file
|