resend-cli 1.2.0 → 1.2.1
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/.github/workflows/release.yml +35 -8
- package/README.md +12 -1
- package/package.json +2 -2
- package/src/cli.ts +12 -4
- package/src/commands/auth/login.ts +3 -28
- package/src/commands/doctor.ts +3 -3
- package/src/commands/teams/remove.ts +2 -5
- package/src/commands/teams/switch.ts +0 -3
- package/src/lib/client.ts +3 -0
- package/src/lib/config.ts +30 -37
- package/tests/commands/auth/login.test.ts +0 -35
- package/tests/lib/config.test.ts +7 -40
- package/.github/workflows/test-build-windows.yml +0 -44
|
@@ -3,13 +3,46 @@ on:
|
|
|
3
3
|
push:
|
|
4
4
|
tags:
|
|
5
5
|
- 'v*'
|
|
6
|
-
permissions:
|
|
7
|
-
contents: write
|
|
8
6
|
concurrency:
|
|
9
7
|
group: release
|
|
10
8
|
cancel-in-progress: false
|
|
11
9
|
jobs:
|
|
10
|
+
test-binary:
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
include:
|
|
15
|
+
- os: macos-latest
|
|
16
|
+
target: bun-darwin-arm64
|
|
17
|
+
binary: dist/resend
|
|
18
|
+
flags: "--bytecode"
|
|
19
|
+
- os: ubuntu-latest
|
|
20
|
+
target: bun-linux-x64
|
|
21
|
+
binary: dist/resend
|
|
22
|
+
flags: "--bytecode"
|
|
23
|
+
- os: windows-latest
|
|
24
|
+
target: bun-windows-x64
|
|
25
|
+
binary: dist/resend.exe
|
|
26
|
+
flags: ""
|
|
27
|
+
runs-on: ${{ matrix.os }}
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/checkout@v6
|
|
30
|
+
|
|
31
|
+
- uses: oven-sh/setup-bun@v2
|
|
32
|
+
|
|
33
|
+
- name: Install dependencies
|
|
34
|
+
run: bun install --frozen-lockfile
|
|
35
|
+
|
|
36
|
+
- name: Build binary
|
|
37
|
+
run: bun build --compile --minify --sourcemap ${{ matrix.flags }} src/cli.ts --target=${{ matrix.target }} --outfile ${{ matrix.binary }}
|
|
38
|
+
|
|
39
|
+
- name: Verify binary runs
|
|
40
|
+
run: ${{ matrix.binary }} --version
|
|
41
|
+
|
|
12
42
|
release:
|
|
43
|
+
needs: test-binary
|
|
44
|
+
permissions:
|
|
45
|
+
contents: write
|
|
13
46
|
runs-on: blacksmith-2vcpu-ubuntu-2204
|
|
14
47
|
steps:
|
|
15
48
|
- uses: actions/checkout@v6
|
|
@@ -64,12 +97,6 @@ jobs:
|
|
|
64
97
|
```pwsh
|
|
65
98
|
irm https://resend.com/install.ps1 | iex
|
|
66
99
|
```
|
|
67
|
-
|
|
68
|
-
**GitHub CLI** _(use while repo is private)_
|
|
69
|
-
```sh
|
|
70
|
-
gh release download --repo resend/resend-cli --pattern "resend-darwin-arm64.tar.gz"
|
|
71
|
-
tar -xzf resend-darwin-arm64.tar.gz && sudo mv resend /usr/local/bin/ && rm resend-darwin-arm64.tar.gz
|
|
72
|
-
```
|
|
73
100
|
files: |
|
|
74
101
|
dist/resend-darwin-arm64.tar.gz
|
|
75
102
|
dist/resend-darwin-x64.tar.gz
|
package/README.md
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
# Resend CLI
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The official CLI for [Resend](https://resend.com).
|
|
4
|
+
|
|
5
|
+
Built for humans, AI agents, and CI/CD pipelines.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
██████╗ ███████╗███████╗███████╗███╗ ██╗██████╗
|
|
9
|
+
██╔══██╗██╔════╝██╔════╝██╔════╝████╗ ██║██╔══██╗
|
|
10
|
+
██████╔╝█████╗ ███████╗█████╗ ██╔██╗ ██║██║ ██║
|
|
11
|
+
██╔══██╗██╔══╝ ╚════██║██╔══╝ ██║╚██╗██║██║ ██║
|
|
12
|
+
██║ ██║███████╗███████║███████╗██║ ╚████║██████╔╝
|
|
13
|
+
╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝╚═╝ ╚═══╝╚═════╝
|
|
14
|
+
```
|
|
4
15
|
|
|
5
16
|
## Install
|
|
6
17
|
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -17,9 +17,19 @@ import { webhooksCommand } from './commands/webhooks/index';
|
|
|
17
17
|
import { whoamiCommand } from './commands/whoami';
|
|
18
18
|
import { PACKAGE_NAME, VERSION } from './lib/version';
|
|
19
19
|
|
|
20
|
+
const BANNER = `
|
|
21
|
+
██████╗ ███████╗███████╗███████╗███╗ ██╗██████╗
|
|
22
|
+
██╔══██╗██╔════╝██╔════╝██╔════╝████╗ ██║██╔══██╗
|
|
23
|
+
██████╔╝█████╗ ███████╗█████╗ ██╔██╗ ██║██║ ██║
|
|
24
|
+
██╔══██╗██╔══╝ ╚════██║██╔══╝ ██║╚██╗██║██║ ██║
|
|
25
|
+
██║ ██║███████╗███████║███████╗██║ ╚████║██████╔╝
|
|
26
|
+
╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝╚═╝ ╚═══╝╚═════╝
|
|
27
|
+
`;
|
|
28
|
+
|
|
20
29
|
const program = new Command()
|
|
21
30
|
.name('resend')
|
|
22
31
|
.description('Resend CLI — email for developers')
|
|
32
|
+
.addHelpText('beforeAll', BANNER)
|
|
23
33
|
.version(
|
|
24
34
|
`${PACKAGE_NAME} v${VERSION}`,
|
|
25
35
|
'-v, --version',
|
|
@@ -50,10 +60,8 @@ Output:
|
|
|
50
60
|
Errors always exit with code 1: {"error":{"message":"...","code":"..."}}
|
|
51
61
|
|
|
52
62
|
Examples:
|
|
53
|
-
$ resend login
|
|
54
|
-
$ resend emails send
|
|
55
|
-
$ resend emails batch --file ./emails.json --json
|
|
56
|
-
$ resend doctor --json`,
|
|
63
|
+
$ resend login
|
|
64
|
+
$ resend emails send`,
|
|
57
65
|
)
|
|
58
66
|
.addCommand(loginCommand)
|
|
59
67
|
.addCommand(logoutCommand)
|
|
@@ -3,13 +3,7 @@ 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 {
|
|
7
|
-
listTeams,
|
|
8
|
-
resolveApiKey,
|
|
9
|
-
setActiveTeam,
|
|
10
|
-
storeApiKey,
|
|
11
|
-
validateTeamName,
|
|
12
|
-
} from '../../lib/config';
|
|
6
|
+
import { listTeams, resolveApiKey, storeApiKey } from '../../lib/config';
|
|
13
7
|
import { buildHelpText } from '../../lib/help-text';
|
|
14
8
|
import { errorMessage, outputError, outputResult } from '../../lib/output';
|
|
15
9
|
import { cancelAndExit } from '../../lib/prompts';
|
|
@@ -156,17 +150,6 @@ export const loginCommand = new Command('login')
|
|
|
156
150
|
|
|
157
151
|
let teamName = globalOpts.team;
|
|
158
152
|
|
|
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
|
-
|
|
170
153
|
if (!teamName && isInteractive()) {
|
|
171
154
|
const existingTeams = listTeams();
|
|
172
155
|
if (existingTeams.length > 0) {
|
|
@@ -190,7 +173,8 @@ export const loginCommand = new Command('login')
|
|
|
190
173
|
if (choice === '__new__') {
|
|
191
174
|
const newName = await p.text({
|
|
192
175
|
message: 'Enter a name for the new team:',
|
|
193
|
-
validate: (v) =>
|
|
176
|
+
validate: (v) =>
|
|
177
|
+
!v || v.length === 0 ? 'Team name is required' : undefined,
|
|
194
178
|
});
|
|
195
179
|
if (p.isCancel(newName)) {
|
|
196
180
|
cancelAndExit('Login cancelled.');
|
|
@@ -207,15 +191,6 @@ export const loginCommand = new Command('login')
|
|
|
207
191
|
const configPath = storeApiKey(apiKey, teamName);
|
|
208
192
|
const teamLabel = teamName || 'default';
|
|
209
193
|
|
|
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
|
-
|
|
219
194
|
if (globalOpts.json) {
|
|
220
195
|
outputResult(
|
|
221
196
|
{ 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(
|
|
70
|
-
const resolved = resolveApiKey(
|
|
69
|
+
function checkApiKeyPresence(): CheckResult {
|
|
70
|
+
const resolved = resolveApiKey();
|
|
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();
|
|
250
250
|
checks.push(keyCheck);
|
|
251
251
|
if (keyCheck.status === 'fail') {
|
|
252
252
|
spinner?.fail(keyCheck.message);
|
|
@@ -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, removeTeam } 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,7 +24,6 @@ export const removeCommand = new Command('remove')
|
|
|
24
24
|
},
|
|
25
25
|
{ json: globalOpts.json },
|
|
26
26
|
);
|
|
27
|
-
return;
|
|
28
27
|
}
|
|
29
28
|
|
|
30
29
|
const teams = listTeams();
|
|
@@ -36,7 +35,6 @@ export const removeCommand = new Command('remove')
|
|
|
36
35
|
},
|
|
37
36
|
{ json: globalOpts.json },
|
|
38
37
|
);
|
|
39
|
-
return;
|
|
40
38
|
}
|
|
41
39
|
|
|
42
40
|
const choice = await p.select({
|
|
@@ -66,7 +64,7 @@ export const removeCommand = new Command('remove')
|
|
|
66
64
|
}
|
|
67
65
|
|
|
68
66
|
try {
|
|
69
|
-
|
|
67
|
+
removeTeam(teamName);
|
|
70
68
|
} catch (err) {
|
|
71
69
|
outputError(
|
|
72
70
|
{
|
|
@@ -75,7 +73,6 @@ export const removeCommand = new Command('remove')
|
|
|
75
73
|
},
|
|
76
74
|
{ json: globalOpts.json },
|
|
77
75
|
);
|
|
78
|
-
return;
|
|
79
76
|
}
|
|
80
77
|
|
|
81
78
|
if (globalOpts.json) {
|
|
@@ -24,7 +24,6 @@ export const switchCommand = new Command('switch')
|
|
|
24
24
|
},
|
|
25
25
|
{ json: globalOpts.json },
|
|
26
26
|
);
|
|
27
|
-
return;
|
|
28
27
|
}
|
|
29
28
|
|
|
30
29
|
const teams = listTeams();
|
|
@@ -36,7 +35,6 @@ export const switchCommand = new Command('switch')
|
|
|
36
35
|
},
|
|
37
36
|
{ json: globalOpts.json },
|
|
38
37
|
);
|
|
39
|
-
return;
|
|
40
38
|
}
|
|
41
39
|
|
|
42
40
|
const choice = await p.select({
|
|
@@ -65,7 +63,6 @@ export const switchCommand = new Command('switch')
|
|
|
65
63
|
},
|
|
66
64
|
{ json: globalOpts.json },
|
|
67
65
|
);
|
|
68
|
-
return;
|
|
69
66
|
}
|
|
70
67
|
|
|
71
68
|
if (globalOpts.json) {
|
package/src/lib/client.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Resend } from 'resend';
|
|
2
2
|
import { resolveApiKey } from './config';
|
|
3
3
|
import { errorMessage, outputError } from './output';
|
|
4
|
+
import { VERSION } from './version';
|
|
4
5
|
|
|
5
6
|
export type GlobalOpts = {
|
|
6
7
|
apiKey?: string;
|
|
@@ -9,6 +10,8 @@ export type GlobalOpts = {
|
|
|
9
10
|
team?: string;
|
|
10
11
|
};
|
|
11
12
|
|
|
13
|
+
process.env.RESEND_USER_AGENT = `resend-cli:${VERSION}`;
|
|
14
|
+
|
|
12
15
|
export function createClient(flagValue?: string, teamName?: string): Resend {
|
|
13
16
|
const resolved = resolveApiKey(flagValue, teamName);
|
|
14
17
|
if (!resolved) {
|
package/src/lib/config.ts
CHANGED
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
chmodSync,
|
|
3
|
-
existsSync,
|
|
4
|
-
mkdirSync,
|
|
5
|
-
readFileSync,
|
|
6
|
-
unlinkSync,
|
|
7
|
-
writeFileSync,
|
|
8
|
-
} from 'node:fs';
|
|
1
|
+
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
9
2
|
import { homedir } from 'node:os';
|
|
10
3
|
import { join } from 'node:path';
|
|
11
4
|
|
|
@@ -109,10 +102,6 @@ export function resolveApiKey(
|
|
|
109
102
|
|
|
110
103
|
export function storeApiKey(apiKey: string, teamName?: string): string {
|
|
111
104
|
const team = teamName || 'default';
|
|
112
|
-
const validationError = validateTeamName(team);
|
|
113
|
-
if (validationError) {
|
|
114
|
-
throw new Error(validationError);
|
|
115
|
-
}
|
|
116
105
|
const creds = readCredentials() || { active_team: 'default', teams: {} };
|
|
117
106
|
|
|
118
107
|
creds.teams[team] = { api_key: apiKey };
|
|
@@ -127,6 +116,7 @@ export function storeApiKey(apiKey: string, teamName?: string): string {
|
|
|
127
116
|
|
|
128
117
|
export function removeAllApiKeys(): string {
|
|
129
118
|
const configPath = getCredentialsPath();
|
|
119
|
+
const { unlinkSync } = require('node:fs');
|
|
130
120
|
unlinkSync(configPath);
|
|
131
121
|
return configPath;
|
|
132
122
|
}
|
|
@@ -135,20 +125,13 @@ export function removeApiKey(teamName?: string): string {
|
|
|
135
125
|
const creds = readCredentials();
|
|
136
126
|
if (!creds) {
|
|
137
127
|
const configPath = getCredentialsPath();
|
|
138
|
-
if (!existsSync(configPath)) {
|
|
139
|
-
throw new Error('No credentials file found.');
|
|
140
|
-
}
|
|
141
128
|
// Try to delete legacy file
|
|
129
|
+
const { unlinkSync } = require('node:fs');
|
|
142
130
|
unlinkSync(configPath);
|
|
143
131
|
return configPath;
|
|
144
132
|
}
|
|
145
133
|
|
|
146
134
|
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
|
-
}
|
|
152
135
|
delete creds.teams[team];
|
|
153
136
|
|
|
154
137
|
// If we removed the active team, switch to first available or "default"
|
|
@@ -159,6 +142,7 @@ export function removeApiKey(teamName?: string): string {
|
|
|
159
142
|
|
|
160
143
|
// If no teams left, delete the file
|
|
161
144
|
if (Object.keys(creds.teams).length === 0) {
|
|
145
|
+
const { unlinkSync } = require('node:fs');
|
|
162
146
|
const configPath = getCredentialsPath();
|
|
163
147
|
unlinkSync(configPath);
|
|
164
148
|
return configPath;
|
|
@@ -168,10 +152,6 @@ export function removeApiKey(teamName?: string): string {
|
|
|
168
152
|
}
|
|
169
153
|
|
|
170
154
|
export function setActiveTeam(teamName: string): void {
|
|
171
|
-
const validationError = validateTeamName(teamName);
|
|
172
|
-
if (validationError) {
|
|
173
|
-
throw new Error(validationError);
|
|
174
|
-
}
|
|
175
155
|
const creds = readCredentials();
|
|
176
156
|
if (!creds) {
|
|
177
157
|
throw new Error('No credentials file found. Run: resend login');
|
|
@@ -196,19 +176,6 @@ export function listTeams(): Array<{ name: string; active: boolean }> {
|
|
|
196
176
|
}));
|
|
197
177
|
}
|
|
198
178
|
|
|
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
|
-
|
|
212
179
|
export function maskKey(key: string): string {
|
|
213
180
|
if (key.length <= 7) {
|
|
214
181
|
return `${key.slice(0, 3)}...`;
|
|
@@ -216,3 +183,29 @@ export function maskKey(key: string): string {
|
|
|
216
183
|
return `${key.slice(0, 3)}...${key.slice(-4)}`;
|
|
217
184
|
}
|
|
218
185
|
|
|
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
|
+
}
|
|
@@ -116,39 +116,4 @@ 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
|
-
});
|
|
154
119
|
});
|
package/tests/lib/config.test.ts
CHANGED
|
@@ -5,12 +5,11 @@ import { join } from 'node:path';
|
|
|
5
5
|
import {
|
|
6
6
|
getConfigDir,
|
|
7
7
|
listTeams,
|
|
8
|
-
|
|
8
|
+
removeTeam,
|
|
9
9
|
resolveApiKey,
|
|
10
10
|
resolveTeamName,
|
|
11
11
|
setActiveTeam,
|
|
12
12
|
storeApiKey,
|
|
13
|
-
validateTeamName,
|
|
14
13
|
} from '../../src/lib/config';
|
|
15
14
|
import { captureTestEnv } from '../helpers';
|
|
16
15
|
|
|
@@ -355,7 +354,7 @@ describe('setActiveTeam', () => {
|
|
|
355
354
|
});
|
|
356
355
|
});
|
|
357
356
|
|
|
358
|
-
describe('
|
|
357
|
+
describe('removeTeam', () => {
|
|
359
358
|
const restoreEnv = captureTestEnv();
|
|
360
359
|
let tmpDir: string;
|
|
361
360
|
|
|
@@ -376,7 +375,7 @@ describe('removeApiKey', () => {
|
|
|
376
375
|
storeApiKey('re_default', 'default');
|
|
377
376
|
storeApiKey('re_staging', 'staging');
|
|
378
377
|
|
|
379
|
-
|
|
378
|
+
removeTeam('staging');
|
|
380
379
|
|
|
381
380
|
const teams = listTeams();
|
|
382
381
|
expect(teams).toEqual([{ name: 'default', active: true }]);
|
|
@@ -387,7 +386,7 @@ describe('removeApiKey', () => {
|
|
|
387
386
|
storeApiKey('re_staging', 'staging');
|
|
388
387
|
setActiveTeam('staging');
|
|
389
388
|
|
|
390
|
-
|
|
389
|
+
removeTeam('staging');
|
|
391
390
|
|
|
392
391
|
const configPath = join(tmpDir, 'resend', 'credentials.json');
|
|
393
392
|
const data = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
@@ -397,7 +396,7 @@ describe('removeApiKey', () => {
|
|
|
397
396
|
test('deletes file when last team removed', () => {
|
|
398
397
|
storeApiKey('re_only', 'only');
|
|
399
398
|
|
|
400
|
-
|
|
399
|
+
removeTeam('only');
|
|
401
400
|
|
|
402
401
|
const { existsSync } = require('node:fs');
|
|
403
402
|
const configPath = join(tmpDir, 'resend', 'credentials.json');
|
|
@@ -406,42 +405,10 @@ describe('removeApiKey', () => {
|
|
|
406
405
|
|
|
407
406
|
test('throws when team does not exist', () => {
|
|
408
407
|
storeApiKey('re_default');
|
|
409
|
-
expect(() =>
|
|
408
|
+
expect(() => removeTeam('nonexistent')).toThrow('not found');
|
|
410
409
|
});
|
|
411
410
|
|
|
412
411
|
test('throws when no credentials file', () => {
|
|
413
|
-
expect(() =>
|
|
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();
|
|
412
|
+
expect(() => removeTeam('any')).toThrow('No credentials file');
|
|
446
413
|
});
|
|
447
414
|
});
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
name: Test Windows Build
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches:
|
|
6
|
-
- main
|
|
7
|
-
pull_request:
|
|
8
|
-
paths:
|
|
9
|
-
- src/**
|
|
10
|
-
- .github/workflows/test-build-windows.yml
|
|
11
|
-
- .github/workflows/release.yml
|
|
12
|
-
- bun.lock
|
|
13
|
-
workflow_dispatch:
|
|
14
|
-
|
|
15
|
-
jobs:
|
|
16
|
-
build:
|
|
17
|
-
runs-on: windows-latest
|
|
18
|
-
steps:
|
|
19
|
-
- uses: actions/checkout@v6
|
|
20
|
-
|
|
21
|
-
- uses: oven-sh/setup-bun@v2
|
|
22
|
-
|
|
23
|
-
- name: Install dependencies
|
|
24
|
-
run: bun install --frozen-lockfile
|
|
25
|
-
|
|
26
|
-
- name: Build Windows binary
|
|
27
|
-
run: bun build --compile --minify --sourcemap src/cli.ts --target=bun-windows-x64 --outfile dist/resend-windows-x64.exe
|
|
28
|
-
|
|
29
|
-
- name: Verify binary exists
|
|
30
|
-
shell: pwsh
|
|
31
|
-
run: |
|
|
32
|
-
if (-not (Test-Path dist\resend-windows-x64.exe)) {
|
|
33
|
-
Write-Error "Binary not found"
|
|
34
|
-
exit 1
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
- name: Verify binary runs
|
|
38
|
-
shell: pwsh
|
|
39
|
-
run: |
|
|
40
|
-
& dist\resend-windows-x64.exe --version
|
|
41
|
-
if ($LASTEXITCODE -ne 0) {
|
|
42
|
-
Write-Error "resend --version failed"
|
|
43
|
-
exit 1
|
|
44
|
-
}
|