resend-cli 1.1.0 → 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 +1 -10
- 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/workflows/ci.yml +3 -3
- package/.github/workflows/pr-title-check.yml +1 -1
- package/.github/workflows/release.yml +2 -2
- package/.github/workflows/test-build-windows.yml +44 -0
- package/.github/workflows/test-install-windows.yml +48 -0
- package/README.md +8 -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 +1 -0
- package/install.sh +11 -4
- package/package.json +1 -1
- package/renovate.json +4 -0
- package/src/cli.ts +9 -0
- package/src/commands/auth/login.ts +34 -13
- package/src/commands/doctor.ts +3 -3
- package/src/commands/open.ts +24 -0
- package/src/commands/teams/remove.ts +5 -2
- package/src/commands/teams/switch.ts +3 -0
- package/src/lib/client.ts +6 -1
- package/src/lib/config.ts +37 -30
- package/src/lib/help-text.ts +4 -2
- package/src/lib/spinner.ts +7 -3
- package/tests/commands/auth/login.test.ts +35 -0
- package/tests/lib/config.test.ts +40 -7
- package/tests/lib/help-text.test.ts +2 -1
|
@@ -3,7 +3,13 @@ import * as p from '@clack/prompts';
|
|
|
3
3
|
import { Command } from '@commander-js/extra-typings';
|
|
4
4
|
import { Resend } from 'resend';
|
|
5
5
|
import type { GlobalOpts } from '../../lib/client';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
listTeams,
|
|
8
|
+
resolveApiKey,
|
|
9
|
+
setActiveTeam,
|
|
10
|
+
storeApiKey,
|
|
11
|
+
validateTeamName,
|
|
12
|
+
} from '../../lib/config';
|
|
7
13
|
import { buildHelpText } from '../../lib/help-text';
|
|
8
14
|
import { errorMessage, outputError, outputResult } from '../../lib/output';
|
|
9
15
|
import { cancelAndExit } from '../../lib/prompts';
|
|
@@ -127,7 +133,11 @@ export const loginCommand = new Command('login')
|
|
|
127
133
|
);
|
|
128
134
|
}
|
|
129
135
|
|
|
130
|
-
const spinner = createSpinner(
|
|
136
|
+
const spinner = createSpinner(
|
|
137
|
+
'Validating API key...',
|
|
138
|
+
'braille',
|
|
139
|
+
globalOpts.quiet,
|
|
140
|
+
);
|
|
131
141
|
|
|
132
142
|
try {
|
|
133
143
|
const resend = new Resend(apiKey);
|
|
@@ -146,6 +156,17 @@ export const loginCommand = new Command('login')
|
|
|
146
156
|
|
|
147
157
|
let teamName = globalOpts.team;
|
|
148
158
|
|
|
159
|
+
if (teamName) {
|
|
160
|
+
const teamError = validateTeamName(teamName);
|
|
161
|
+
if (teamError) {
|
|
162
|
+
outputError(
|
|
163
|
+
{ message: teamError, code: 'invalid_team_name' },
|
|
164
|
+
{ json: globalOpts.json },
|
|
165
|
+
);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
149
170
|
if (!teamName && isInteractive()) {
|
|
150
171
|
const existingTeams = listTeams();
|
|
151
172
|
if (existingTeams.length > 0) {
|
|
@@ -169,8 +190,7 @@ export const loginCommand = new Command('login')
|
|
|
169
190
|
if (choice === '__new__') {
|
|
170
191
|
const newName = await p.text({
|
|
171
192
|
message: 'Enter a name for the new team:',
|
|
172
|
-
validate: (v) =>
|
|
173
|
-
!v || v.length === 0 ? 'Team name is required' : undefined,
|
|
193
|
+
validate: (v) => validateTeamName(v),
|
|
174
194
|
});
|
|
175
195
|
if (p.isCancel(newName)) {
|
|
176
196
|
cancelAndExit('Login cancelled.');
|
|
@@ -180,21 +200,22 @@ export const loginCommand = new Command('login')
|
|
|
180
200
|
teamName = choice;
|
|
181
201
|
}
|
|
182
202
|
} else {
|
|
183
|
-
|
|
184
|
-
message: 'Enter a team name (or press Enter for "default"):',
|
|
185
|
-
defaultValue: 'default',
|
|
186
|
-
placeholder: 'default',
|
|
187
|
-
});
|
|
188
|
-
if (p.isCancel(newName)) {
|
|
189
|
-
cancelAndExit('Login cancelled.');
|
|
190
|
-
}
|
|
191
|
-
teamName = newName;
|
|
203
|
+
teamName = 'default';
|
|
192
204
|
}
|
|
193
205
|
}
|
|
194
206
|
|
|
195
207
|
const configPath = storeApiKey(apiKey, teamName);
|
|
196
208
|
const teamLabel = teamName || 'default';
|
|
197
209
|
|
|
210
|
+
// Auto-switch to the newly added team
|
|
211
|
+
if (teamName) {
|
|
212
|
+
try {
|
|
213
|
+
setActiveTeam(teamName);
|
|
214
|
+
} catch {
|
|
215
|
+
// Team was just stored, so this should not fail
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
198
219
|
if (globalOpts.json) {
|
|
199
220
|
outputResult(
|
|
200
221
|
{ success: true, config_path: configPath, team: teamLabel },
|
package/src/commands/doctor.ts
CHANGED
|
@@ -66,8 +66,8 @@ async function checkCliVersion(): Promise<CheckResult> {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
function checkApiKeyPresence(): CheckResult {
|
|
70
|
-
const resolved = resolveApiKey();
|
|
69
|
+
function checkApiKeyPresence(flagValue?: string): CheckResult {
|
|
70
|
+
const resolved = resolveApiKey(flagValue);
|
|
71
71
|
if (!resolved) {
|
|
72
72
|
return {
|
|
73
73
|
name: 'API Key',
|
|
@@ -246,7 +246,7 @@ export const doctorCommand = new Command('doctor')
|
|
|
246
246
|
|
|
247
247
|
// Check 2: API Key
|
|
248
248
|
spinner = interactive ? createSpinner('Checking API key...', 'scan') : null;
|
|
249
|
-
const keyCheck = checkApiKeyPresence();
|
|
249
|
+
const keyCheck = checkApiKeyPresence(globalOpts.apiKey);
|
|
250
250
|
checks.push(keyCheck);
|
|
251
251
|
if (keyCheck.status === 'fail') {
|
|
252
252
|
spinner?.fail(keyCheck.message);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Command } from '@commander-js/extra-typings';
|
|
2
|
+
import { buildHelpText } from '../lib/help-text';
|
|
3
|
+
|
|
4
|
+
export const openCommand = new Command('open')
|
|
5
|
+
.description('Open the Resend dashboard in your browser')
|
|
6
|
+
.addHelpText(
|
|
7
|
+
'after',
|
|
8
|
+
buildHelpText({
|
|
9
|
+
context: 'Opens https://resend.com/emails in your default browser.',
|
|
10
|
+
examples: ['resend open'],
|
|
11
|
+
}),
|
|
12
|
+
)
|
|
13
|
+
.action(async () => {
|
|
14
|
+
const url = 'https://resend.com/emails';
|
|
15
|
+
const { platform } = process;
|
|
16
|
+
const args =
|
|
17
|
+
platform === 'darwin'
|
|
18
|
+
? ['open', url]
|
|
19
|
+
: platform === 'win32'
|
|
20
|
+
? ['cmd', '/c', 'start', url]
|
|
21
|
+
: ['xdg-open', url];
|
|
22
|
+
|
|
23
|
+
Bun.spawn(args, { stdio: ['ignore', 'ignore', 'ignore'] });
|
|
24
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as p from '@clack/prompts';
|
|
2
2
|
import { Command } from '@commander-js/extra-typings';
|
|
3
3
|
import type { GlobalOpts } from '../../lib/client';
|
|
4
|
-
import { listTeams,
|
|
4
|
+
import { listTeams, removeApiKey } from '../../lib/config';
|
|
5
5
|
import { errorMessage, outputError, outputResult } from '../../lib/output';
|
|
6
6
|
import { cancelAndExit } from '../../lib/prompts';
|
|
7
7
|
import { isInteractive } from '../../lib/tty';
|
|
@@ -24,6 +24,7 @@ export const removeCommand = new Command('remove')
|
|
|
24
24
|
},
|
|
25
25
|
{ json: globalOpts.json },
|
|
26
26
|
);
|
|
27
|
+
return;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
const teams = listTeams();
|
|
@@ -35,6 +36,7 @@ export const removeCommand = new Command('remove')
|
|
|
35
36
|
},
|
|
36
37
|
{ json: globalOpts.json },
|
|
37
38
|
);
|
|
39
|
+
return;
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
const choice = await p.select({
|
|
@@ -64,7 +66,7 @@ export const removeCommand = new Command('remove')
|
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
try {
|
|
67
|
-
|
|
69
|
+
removeApiKey(teamName);
|
|
68
70
|
} catch (err) {
|
|
69
71
|
outputError(
|
|
70
72
|
{
|
|
@@ -73,6 +75,7 @@ export const removeCommand = new Command('remove')
|
|
|
73
75
|
},
|
|
74
76
|
{ json: globalOpts.json },
|
|
75
77
|
);
|
|
78
|
+
return;
|
|
76
79
|
}
|
|
77
80
|
|
|
78
81
|
if (globalOpts.json) {
|
|
@@ -24,6 +24,7 @@ export const switchCommand = new Command('switch')
|
|
|
24
24
|
},
|
|
25
25
|
{ json: globalOpts.json },
|
|
26
26
|
);
|
|
27
|
+
return;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
const teams = listTeams();
|
|
@@ -35,6 +36,7 @@ export const switchCommand = new Command('switch')
|
|
|
35
36
|
},
|
|
36
37
|
{ json: globalOpts.json },
|
|
37
38
|
);
|
|
39
|
+
return;
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
const choice = await p.select({
|
|
@@ -63,6 +65,7 @@ export const switchCommand = new Command('switch')
|
|
|
63
65
|
},
|
|
64
66
|
{ json: globalOpts.json },
|
|
65
67
|
);
|
|
68
|
+
return;
|
|
66
69
|
}
|
|
67
70
|
|
|
68
71
|
if (globalOpts.json) {
|
package/src/lib/client.ts
CHANGED
|
@@ -2,7 +2,12 @@ import { Resend } from 'resend';
|
|
|
2
2
|
import { resolveApiKey } from './config';
|
|
3
3
|
import { errorMessage, outputError } from './output';
|
|
4
4
|
|
|
5
|
-
export type GlobalOpts = {
|
|
5
|
+
export type GlobalOpts = {
|
|
6
|
+
apiKey?: string;
|
|
7
|
+
json?: boolean;
|
|
8
|
+
quiet?: boolean;
|
|
9
|
+
team?: string;
|
|
10
|
+
};
|
|
6
11
|
|
|
7
12
|
export function createClient(flagValue?: string, teamName?: string): Resend {
|
|
8
13
|
const resolved = resolveApiKey(flagValue, teamName);
|
package/src/lib/config.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
chmodSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
unlinkSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from 'node:fs';
|
|
2
9
|
import { homedir } from 'node:os';
|
|
3
10
|
import { join } from 'node:path';
|
|
4
11
|
|
|
@@ -102,6 +109,10 @@ export function resolveApiKey(
|
|
|
102
109
|
|
|
103
110
|
export function storeApiKey(apiKey: string, teamName?: string): string {
|
|
104
111
|
const team = teamName || 'default';
|
|
112
|
+
const validationError = validateTeamName(team);
|
|
113
|
+
if (validationError) {
|
|
114
|
+
throw new Error(validationError);
|
|
115
|
+
}
|
|
105
116
|
const creds = readCredentials() || { active_team: 'default', teams: {} };
|
|
106
117
|
|
|
107
118
|
creds.teams[team] = { api_key: apiKey };
|
|
@@ -116,7 +127,6 @@ export function storeApiKey(apiKey: string, teamName?: string): string {
|
|
|
116
127
|
|
|
117
128
|
export function removeAllApiKeys(): string {
|
|
118
129
|
const configPath = getCredentialsPath();
|
|
119
|
-
const { unlinkSync } = require('node:fs');
|
|
120
130
|
unlinkSync(configPath);
|
|
121
131
|
return configPath;
|
|
122
132
|
}
|
|
@@ -125,13 +135,20 @@ export function removeApiKey(teamName?: string): string {
|
|
|
125
135
|
const creds = readCredentials();
|
|
126
136
|
if (!creds) {
|
|
127
137
|
const configPath = getCredentialsPath();
|
|
138
|
+
if (!existsSync(configPath)) {
|
|
139
|
+
throw new Error('No credentials file found.');
|
|
140
|
+
}
|
|
128
141
|
// Try to delete legacy file
|
|
129
|
-
const { unlinkSync } = require('node:fs');
|
|
130
142
|
unlinkSync(configPath);
|
|
131
143
|
return configPath;
|
|
132
144
|
}
|
|
133
145
|
|
|
134
146
|
const team = teamName || resolveTeamName();
|
|
147
|
+
if (!creds.teams[team]) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
`Team "${team}" not found. Available teams: ${Object.keys(creds.teams).join(', ')}`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
135
152
|
delete creds.teams[team];
|
|
136
153
|
|
|
137
154
|
// If we removed the active team, switch to first available or "default"
|
|
@@ -142,7 +159,6 @@ export function removeApiKey(teamName?: string): string {
|
|
|
142
159
|
|
|
143
160
|
// If no teams left, delete the file
|
|
144
161
|
if (Object.keys(creds.teams).length === 0) {
|
|
145
|
-
const { unlinkSync } = require('node:fs');
|
|
146
162
|
const configPath = getCredentialsPath();
|
|
147
163
|
unlinkSync(configPath);
|
|
148
164
|
return configPath;
|
|
@@ -152,6 +168,10 @@ export function removeApiKey(teamName?: string): string {
|
|
|
152
168
|
}
|
|
153
169
|
|
|
154
170
|
export function setActiveTeam(teamName: string): void {
|
|
171
|
+
const validationError = validateTeamName(teamName);
|
|
172
|
+
if (validationError) {
|
|
173
|
+
throw new Error(validationError);
|
|
174
|
+
}
|
|
155
175
|
const creds = readCredentials();
|
|
156
176
|
if (!creds) {
|
|
157
177
|
throw new Error('No credentials file found. Run: resend login');
|
|
@@ -176,6 +196,19 @@ export function listTeams(): Array<{ name: string; active: boolean }> {
|
|
|
176
196
|
}));
|
|
177
197
|
}
|
|
178
198
|
|
|
199
|
+
export function validateTeamName(name: string): string | undefined {
|
|
200
|
+
if (!name || name.length === 0) {
|
|
201
|
+
return 'Team name must not be empty';
|
|
202
|
+
}
|
|
203
|
+
if (name.length > 64) {
|
|
204
|
+
return 'Team name must be 64 characters or fewer';
|
|
205
|
+
}
|
|
206
|
+
if (!/^[a-z0-9_-]+$/.test(name)) {
|
|
207
|
+
return 'Team name must contain only lowercase letters, numbers, dashes, and underscores';
|
|
208
|
+
}
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
|
|
179
212
|
export function maskKey(key: string): string {
|
|
180
213
|
if (key.length <= 7) {
|
|
181
214
|
return `${key.slice(0, 3)}...`;
|
|
@@ -183,29 +216,3 @@ export function maskKey(key: string): string {
|
|
|
183
216
|
return `${key.slice(0, 3)}...${key.slice(-4)}`;
|
|
184
217
|
}
|
|
185
218
|
|
|
186
|
-
export function removeTeam(teamName: string): void {
|
|
187
|
-
const creds = readCredentials();
|
|
188
|
-
if (!creds) {
|
|
189
|
-
throw new Error('No credentials file found.');
|
|
190
|
-
}
|
|
191
|
-
if (!creds.teams[teamName]) {
|
|
192
|
-
throw new Error(
|
|
193
|
-
`Team "${teamName}" not found. Available teams: ${Object.keys(creds.teams).join(', ')}`,
|
|
194
|
-
);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
delete creds.teams[teamName];
|
|
198
|
-
|
|
199
|
-
if (creds.active_team === teamName) {
|
|
200
|
-
const remaining = Object.keys(creds.teams);
|
|
201
|
-
creds.active_team = remaining[0] || 'default';
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
if (Object.keys(creds.teams).length === 0) {
|
|
205
|
-
const { unlinkSync } = require('node:fs');
|
|
206
|
-
unlinkSync(getCredentialsPath());
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
writeCredentials(creds);
|
|
211
|
-
}
|
package/src/lib/help-text.ts
CHANGED
|
@@ -9,11 +9,13 @@ export interface HelpTextOptions {
|
|
|
9
9
|
const GLOBAL_OPTS_FULL = `Global options:
|
|
10
10
|
--api-key <key> API key (or set RESEND_API_KEY env var)
|
|
11
11
|
--team <name> Team profile to use (overrides RESEND_TEAM)
|
|
12
|
-
--json Force JSON output (also auto-enabled when stdout is piped)
|
|
12
|
+
--json Force JSON output (also auto-enabled when stdout is piped)
|
|
13
|
+
-q, --quiet Suppress spinners and status output (implies --json)`;
|
|
13
14
|
|
|
14
15
|
const GLOBAL_OPTS_SETUP = `Global options:
|
|
15
16
|
--team <name> Team profile to save the key to
|
|
16
|
-
--json Force JSON output
|
|
17
|
+
--json Force JSON output
|
|
18
|
+
-q, --quiet Suppress spinners and status output (implies --json)`;
|
|
17
19
|
|
|
18
20
|
const ERROR_ENVELOPE = ` {"error":{"message":"<message>","code":"<code>"}}`;
|
|
19
21
|
|
package/src/lib/spinner.ts
CHANGED
|
@@ -21,7 +21,7 @@ export async function withSpinner<T>(
|
|
|
21
21
|
errorCode: string,
|
|
22
22
|
globalOpts: GlobalOpts,
|
|
23
23
|
): Promise<T> {
|
|
24
|
-
const spinner = createSpinner(messages.loading);
|
|
24
|
+
const spinner = createSpinner(messages.loading, 'braille', globalOpts.quiet);
|
|
25
25
|
try {
|
|
26
26
|
const { data, error } = await call();
|
|
27
27
|
if (error) {
|
|
@@ -51,8 +51,12 @@ export async function withSpinner<T>(
|
|
|
51
51
|
|
|
52
52
|
export type SpinnerName = keyof typeof spinners;
|
|
53
53
|
|
|
54
|
-
export function createSpinner(
|
|
55
|
-
|
|
54
|
+
export function createSpinner(
|
|
55
|
+
message: string,
|
|
56
|
+
name: SpinnerName = 'braille',
|
|
57
|
+
quiet?: boolean,
|
|
58
|
+
) {
|
|
59
|
+
if (quiet || !isInteractive()) {
|
|
56
60
|
return {
|
|
57
61
|
update(_msg: string) {},
|
|
58
62
|
stop(_msg: string) {},
|
|
@@ -116,4 +116,39 @@ describe('login command', () => {
|
|
|
116
116
|
// Original team should still exist
|
|
117
117
|
expect(data.teams.production.api_key).toBe('re_old_key_1234');
|
|
118
118
|
});
|
|
119
|
+
|
|
120
|
+
// This test must be last — addCommand permanently modifies the shared loginCommand singleton
|
|
121
|
+
test('auto-switches to team specified via --team flag', async () => {
|
|
122
|
+
spies = setupOutputSpies();
|
|
123
|
+
|
|
124
|
+
const { Command } = await import('@commander-js/extra-typings');
|
|
125
|
+
const { loginCommand } = await import('../../../src/commands/auth/login');
|
|
126
|
+
const program = new Command()
|
|
127
|
+
.option('--team <name>')
|
|
128
|
+
.option('--json')
|
|
129
|
+
.option('--api-key <key>')
|
|
130
|
+
.option('-q, --quiet')
|
|
131
|
+
.addCommand(loginCommand);
|
|
132
|
+
|
|
133
|
+
// First store a default key
|
|
134
|
+
const configDir = join(tmpDir, 'resend');
|
|
135
|
+
mkdirSync(configDir, { recursive: true });
|
|
136
|
+
writeFileSync(
|
|
137
|
+
join(configDir, 'credentials.json'),
|
|
138
|
+
JSON.stringify({
|
|
139
|
+
active_team: 'default',
|
|
140
|
+
teams: { default: { api_key: 're_old_key_1234' } },
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
await program.parseAsync(
|
|
145
|
+
['login', '--key', 're_staging_key_123', '--team', 'staging'],
|
|
146
|
+
{ from: 'user' },
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const configPath = join(tmpDir, 'resend', 'credentials.json');
|
|
150
|
+
const data = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
151
|
+
expect(data.active_team).toBe('staging');
|
|
152
|
+
expect(data.teams.staging.api_key).toBe('re_staging_key_123');
|
|
153
|
+
});
|
|
119
154
|
});
|
package/tests/lib/config.test.ts
CHANGED
|
@@ -5,11 +5,12 @@ import { join } from 'node:path';
|
|
|
5
5
|
import {
|
|
6
6
|
getConfigDir,
|
|
7
7
|
listTeams,
|
|
8
|
-
|
|
8
|
+
removeApiKey,
|
|
9
9
|
resolveApiKey,
|
|
10
10
|
resolveTeamName,
|
|
11
11
|
setActiveTeam,
|
|
12
12
|
storeApiKey,
|
|
13
|
+
validateTeamName,
|
|
13
14
|
} from '../../src/lib/config';
|
|
14
15
|
import { captureTestEnv } from '../helpers';
|
|
15
16
|
|
|
@@ -354,7 +355,7 @@ describe('setActiveTeam', () => {
|
|
|
354
355
|
});
|
|
355
356
|
});
|
|
356
357
|
|
|
357
|
-
describe('
|
|
358
|
+
describe('removeApiKey', () => {
|
|
358
359
|
const restoreEnv = captureTestEnv();
|
|
359
360
|
let tmpDir: string;
|
|
360
361
|
|
|
@@ -375,7 +376,7 @@ describe('removeTeam', () => {
|
|
|
375
376
|
storeApiKey('re_default', 'default');
|
|
376
377
|
storeApiKey('re_staging', 'staging');
|
|
377
378
|
|
|
378
|
-
|
|
379
|
+
removeApiKey('staging');
|
|
379
380
|
|
|
380
381
|
const teams = listTeams();
|
|
381
382
|
expect(teams).toEqual([{ name: 'default', active: true }]);
|
|
@@ -386,7 +387,7 @@ describe('removeTeam', () => {
|
|
|
386
387
|
storeApiKey('re_staging', 'staging');
|
|
387
388
|
setActiveTeam('staging');
|
|
388
389
|
|
|
389
|
-
|
|
390
|
+
removeApiKey('staging');
|
|
390
391
|
|
|
391
392
|
const configPath = join(tmpDir, 'resend', 'credentials.json');
|
|
392
393
|
const data = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
@@ -396,7 +397,7 @@ describe('removeTeam', () => {
|
|
|
396
397
|
test('deletes file when last team removed', () => {
|
|
397
398
|
storeApiKey('re_only', 'only');
|
|
398
399
|
|
|
399
|
-
|
|
400
|
+
removeApiKey('only');
|
|
400
401
|
|
|
401
402
|
const { existsSync } = require('node:fs');
|
|
402
403
|
const configPath = join(tmpDir, 'resend', 'credentials.json');
|
|
@@ -405,10 +406,42 @@ describe('removeTeam', () => {
|
|
|
405
406
|
|
|
406
407
|
test('throws when team does not exist', () => {
|
|
407
408
|
storeApiKey('re_default');
|
|
408
|
-
expect(() =>
|
|
409
|
+
expect(() => removeApiKey('nonexistent')).toThrow('not found');
|
|
409
410
|
});
|
|
410
411
|
|
|
411
412
|
test('throws when no credentials file', () => {
|
|
412
|
-
expect(() =>
|
|
413
|
+
expect(() => removeApiKey('any')).toThrow('No credentials file');
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
describe('validateTeamName', () => {
|
|
418
|
+
test('accepts valid names', () => {
|
|
419
|
+
expect(validateTeamName('default')).toBeUndefined();
|
|
420
|
+
expect(validateTeamName('my-team')).toBeUndefined();
|
|
421
|
+
expect(validateTeamName('team_1')).toBeUndefined();
|
|
422
|
+
expect(validateTeamName('prod-2024')).toBeUndefined();
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test('rejects uppercase characters', () => {
|
|
426
|
+
expect(validateTeamName('Production')).toContain('lowercase');
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
test('rejects spaces and special characters', () => {
|
|
430
|
+
expect(validateTeamName('my team')).toContain('lowercase');
|
|
431
|
+
expect(validateTeamName('team@org')).toContain('lowercase');
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test('rejects empty name', () => {
|
|
435
|
+
expect(validateTeamName('')).toContain('empty');
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
test('rejects names longer than 64 characters', () => {
|
|
439
|
+
const longName = 'a'.repeat(65);
|
|
440
|
+
expect(validateTeamName(longName)).toContain('64');
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
test('accepts name exactly 64 characters', () => {
|
|
444
|
+
const maxName = 'a'.repeat(64);
|
|
445
|
+
expect(validateTeamName(maxName)).toBeUndefined();
|
|
413
446
|
});
|
|
414
447
|
});
|
|
@@ -20,7 +20,8 @@ describe('buildHelpText', () => {
|
|
|
20
20
|
'Global options:\n' +
|
|
21
21
|
' --api-key <key> API key (or set RESEND_API_KEY env var)\n' +
|
|
22
22
|
' --team <name> Team profile to use (overrides RESEND_TEAM)\n' +
|
|
23
|
-
' --json Force JSON output (also auto-enabled when stdout is piped)' +
|
|
23
|
+
' --json Force JSON output (also auto-enabled when stdout is piped)\n' +
|
|
24
|
+
' -q, --quiet Suppress spinners and status output (implies --json)' +
|
|
24
25
|
'\n\n' +
|
|
25
26
|
'Output (--json or piped):\n' +
|
|
26
27
|
' {"id":"em_123"}' +
|