resend-cli 1.1.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/.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 +37 -10
- package/.github/workflows/test-install-windows.yml +48 -0
- package/README.md +20 -1
- 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 +2 -2
- package/renovate.json +4 -0
- package/src/cli.ts +21 -4
- package/src/commands/auth/login.ts +6 -10
- package/src/commands/open.ts +24 -0
- package/src/lib/client.ts +9 -1
- package/src/lib/help-text.ts +4 -2
- package/src/lib/spinner.ts +7 -3
- package/tests/lib/help-text.test.ts +2 -1
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import { Command } from '@commander-js/extra-typings';
|
|
3
|
+
import type { Resend } from 'resend';
|
|
4
|
+
import type { GlobalOpts } from '../../lib/client';
|
|
5
|
+
import { requireClient } from '../../lib/client';
|
|
6
|
+
import { readFile } from '../../lib/files';
|
|
7
|
+
import { buildHelpText } from '../../lib/help-text';
|
|
8
|
+
import { outputResult } from '../../lib/output';
|
|
9
|
+
import {
|
|
10
|
+
cancelAndExit,
|
|
11
|
+
promptForMissing,
|
|
12
|
+
requireText,
|
|
13
|
+
} from '../../lib/prompts';
|
|
14
|
+
import { withSpinner } from '../../lib/spinner';
|
|
15
|
+
import { isInteractive } from '../../lib/tty';
|
|
16
|
+
|
|
17
|
+
export async function fetchVerifiedDomains(resend: Resend): Promise<string[]> {
|
|
18
|
+
try {
|
|
19
|
+
const { data, error } = await resend.domains.list();
|
|
20
|
+
if (error || !data) {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
return data.data
|
|
24
|
+
.filter(
|
|
25
|
+
(d) => d.status === 'verified' && d.capabilities.sending === 'enabled',
|
|
26
|
+
)
|
|
27
|
+
.map((d) => d.name);
|
|
28
|
+
} catch {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const FROM_PREFIXES = ['noreply', 'hello', 'hi', 'info', 'support', 'team'];
|
|
34
|
+
|
|
35
|
+
async function promptForFromAddress(domains: string[]): Promise<string> {
|
|
36
|
+
let domain: string;
|
|
37
|
+
if (domains.length === 1) {
|
|
38
|
+
domain = domains[0];
|
|
39
|
+
} else {
|
|
40
|
+
const result = await p.select({
|
|
41
|
+
message: 'Select a verified domain',
|
|
42
|
+
options: domains.map((d) => ({ value: d, label: d })),
|
|
43
|
+
});
|
|
44
|
+
if (p.isCancel(result)) {
|
|
45
|
+
cancelAndExit('Send cancelled.');
|
|
46
|
+
}
|
|
47
|
+
domain = result;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const options: Array<{ value: string | null; label: string }> =
|
|
51
|
+
FROM_PREFIXES.map((prefix) => ({
|
|
52
|
+
value: `${prefix}@${domain}`,
|
|
53
|
+
label: `${prefix}@${domain}`,
|
|
54
|
+
}));
|
|
55
|
+
options.push({ value: null, label: 'Custom address...' });
|
|
56
|
+
|
|
57
|
+
const result = await p.select({
|
|
58
|
+
message: `From address (@${domain})`,
|
|
59
|
+
options,
|
|
60
|
+
});
|
|
61
|
+
if (p.isCancel(result)) {
|
|
62
|
+
cancelAndExit('Send cancelled.');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (result === null) {
|
|
66
|
+
const custom = await p.text({
|
|
67
|
+
message: 'From address',
|
|
68
|
+
placeholder: `you@${domain}`,
|
|
69
|
+
validate: (v) =>
|
|
70
|
+
!v || !v.includes('@') ? 'Enter a valid email address' : undefined,
|
|
71
|
+
});
|
|
72
|
+
if (p.isCancel(custom)) {
|
|
73
|
+
cancelAndExit('Send cancelled.');
|
|
74
|
+
}
|
|
75
|
+
return custom;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const sendCommand = new Command('send')
|
|
82
|
+
.description('Send an email')
|
|
83
|
+
.option('--from <address>', 'Sender address (required)')
|
|
84
|
+
.option('--to <addresses...>', 'Recipient address(es) (required)')
|
|
85
|
+
.option('--subject <subject>', 'Email subject (required)')
|
|
86
|
+
.option('--html <html>', 'HTML body')
|
|
87
|
+
.option('--html-file <path>', 'Path to an HTML file for the body')
|
|
88
|
+
.option('--text <text>', 'Plain-text body')
|
|
89
|
+
.option('--cc <addresses...>', 'CC recipients')
|
|
90
|
+
.option('--bcc <addresses...>', 'BCC recipients')
|
|
91
|
+
.option('--reply-to <address>', 'Reply-to address')
|
|
92
|
+
.addHelpText(
|
|
93
|
+
'after',
|
|
94
|
+
buildHelpText({
|
|
95
|
+
context:
|
|
96
|
+
'Required: --from, --to, --subject, and one of --text | --html | --html-file',
|
|
97
|
+
output: ' {"id":"<email-id>"}',
|
|
98
|
+
errorCodes: ['auth_error', 'missing_body', 'send_error'],
|
|
99
|
+
examples: [
|
|
100
|
+
'resend emails send --from you@domain.com --to user@example.com --subject "Hello" --text "Hi"',
|
|
101
|
+
'resend emails send --from you@domain.com --to a@example.com --to b@example.com --subject "Hi" --html "<b>Hi</b>" --json',
|
|
102
|
+
'resend emails send --from you@domain.com --to user@example.com --subject "Hi" --html-file ./email.html --json',
|
|
103
|
+
'RESEND_API_KEY=re_123 resend emails send --from you@domain.com --to user@example.com --subject "Hi" --text "Hi"',
|
|
104
|
+
],
|
|
105
|
+
}),
|
|
106
|
+
)
|
|
107
|
+
.action(async (opts, cmd) => {
|
|
108
|
+
const globalOpts = cmd.optsWithGlobals() as GlobalOpts;
|
|
109
|
+
|
|
110
|
+
const resend = requireClient(globalOpts);
|
|
111
|
+
|
|
112
|
+
// Only fetch verified domains in interactive mode — non-interactive
|
|
113
|
+
// callers (CI, agents, scripts) must pass --from explicitly.
|
|
114
|
+
let fromAddress = opts.from;
|
|
115
|
+
if (!fromAddress && isInteractive()) {
|
|
116
|
+
const domains = await fetchVerifiedDomains(resend);
|
|
117
|
+
if (domains.length > 0) {
|
|
118
|
+
fromAddress = await promptForFromAddress(domains);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const filled = await promptForMissing(
|
|
123
|
+
{ from: fromAddress, to: opts.to?.[0], subject: opts.subject },
|
|
124
|
+
[
|
|
125
|
+
{
|
|
126
|
+
flag: 'from',
|
|
127
|
+
message: 'From address',
|
|
128
|
+
placeholder: 'you@example.com',
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
flag: 'to',
|
|
132
|
+
message: 'To address',
|
|
133
|
+
placeholder: 'recipient@example.com',
|
|
134
|
+
},
|
|
135
|
+
{ flag: 'subject', message: 'Subject', placeholder: 'Hello!' },
|
|
136
|
+
],
|
|
137
|
+
globalOpts,
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
let html = opts.html;
|
|
141
|
+
const text = opts.text;
|
|
142
|
+
|
|
143
|
+
if (opts.htmlFile) {
|
|
144
|
+
html = readFile(opts.htmlFile, globalOpts);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let body: string | undefined = text;
|
|
148
|
+
if (!html && !text) {
|
|
149
|
+
body = await requireText(
|
|
150
|
+
undefined,
|
|
151
|
+
{
|
|
152
|
+
message: 'Email body (plain text)',
|
|
153
|
+
placeholder: 'Type your message...',
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
message: 'Missing email body. Provide --html, --html-file, or --text',
|
|
157
|
+
code: 'missing_body',
|
|
158
|
+
},
|
|
159
|
+
globalOpts,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const toAddresses = opts.to ?? [filled.to];
|
|
164
|
+
|
|
165
|
+
const data = await withSpinner(
|
|
166
|
+
{
|
|
167
|
+
loading: 'Sending email...',
|
|
168
|
+
success: 'Email sent',
|
|
169
|
+
fail: 'Failed to send email',
|
|
170
|
+
},
|
|
171
|
+
() =>
|
|
172
|
+
resend.emails.send({
|
|
173
|
+
from: filled.from,
|
|
174
|
+
to: toAddresses,
|
|
175
|
+
subject: filled.subject,
|
|
176
|
+
...(html ? { html } : { text: body as string }),
|
|
177
|
+
...(opts.cc && { cc: opts.cc }),
|
|
178
|
+
...(opts.bcc && { bcc: opts.bcc }),
|
|
179
|
+
...(opts.replyTo && { replyTo: opts.replyTo }),
|
|
180
|
+
}),
|
|
181
|
+
'send_error',
|
|
182
|
+
globalOpts,
|
|
183
|
+
);
|
|
184
|
+
if (!globalOpts.json && isInteractive()) {
|
|
185
|
+
console.log(`\nEmail sent: ${data.id}`);
|
|
186
|
+
} else {
|
|
187
|
+
outputResult(data, { json: globalOpts.json });
|
|
188
|
+
}
|
|
189
|
+
});
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Command } from '@commander-js/extra-typings';
|
|
2
|
+
import { runCreate } from '../../lib/actions';
|
|
3
|
+
import type { GlobalOpts } from '../../lib/client';
|
|
4
|
+
import { buildHelpText } from '../../lib/help-text';
|
|
5
|
+
import { requireText } from '../../lib/prompts';
|
|
6
|
+
|
|
7
|
+
export const createSegmentCommand = new Command('create')
|
|
8
|
+
.description('Create a new segment')
|
|
9
|
+
.option('--name <name>', 'Segment name (required)')
|
|
10
|
+
.addHelpText(
|
|
11
|
+
'after',
|
|
12
|
+
buildHelpText({
|
|
13
|
+
context: `Segments are named groups of contacts. Broadcasts target segments via segment_id.
|
|
14
|
+
Contacts can belong to multiple segments. Audiences are deprecated — use segments instead.
|
|
15
|
+
|
|
16
|
+
Non-interactive: --name is required.`,
|
|
17
|
+
output: ` {"object":"segment","id":"<uuid>","name":"<name>"}`,
|
|
18
|
+
errorCodes: ['auth_error', 'missing_name', 'create_error'],
|
|
19
|
+
examples: [
|
|
20
|
+
'resend segments create --name "Newsletter Subscribers"',
|
|
21
|
+
'resend segments create --name "Beta Users" --json',
|
|
22
|
+
],
|
|
23
|
+
}),
|
|
24
|
+
)
|
|
25
|
+
.action(async (opts, cmd) => {
|
|
26
|
+
const globalOpts = cmd.optsWithGlobals() as GlobalOpts;
|
|
27
|
+
|
|
28
|
+
const name = await requireText(
|
|
29
|
+
opts.name,
|
|
30
|
+
{ message: 'Segment name', placeholder: 'Newsletter Subscribers' },
|
|
31
|
+
{ message: 'Missing --name flag.', code: 'missing_name' },
|
|
32
|
+
globalOpts,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
await runCreate(
|
|
36
|
+
{
|
|
37
|
+
spinner: {
|
|
38
|
+
loading: 'Creating segment...',
|
|
39
|
+
success: 'Segment created',
|
|
40
|
+
fail: 'Failed to create segment',
|
|
41
|
+
},
|
|
42
|
+
sdkCall: (resend) => resend.segments.create({ name }),
|
|
43
|
+
onInteractive: (data) => {
|
|
44
|
+
console.log(`\nSegment created: ${data.id}`);
|
|
45
|
+
console.log(`Name: ${data.name}`);
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
globalOpts,
|
|
49
|
+
);
|
|
50
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Command } from '@commander-js/extra-typings';
|
|
2
|
+
import { runDelete } from '../../lib/actions';
|
|
3
|
+
import type { GlobalOpts } from '../../lib/client';
|
|
4
|
+
import { buildHelpText } from '../../lib/help-text';
|
|
5
|
+
|
|
6
|
+
export const deleteSegmentCommand = new Command('delete')
|
|
7
|
+
.alias('rm')
|
|
8
|
+
.description('Delete a segment')
|
|
9
|
+
.argument('<id>', 'Segment UUID')
|
|
10
|
+
.option(
|
|
11
|
+
'--yes',
|
|
12
|
+
'Skip the confirmation prompt (required in non-interactive mode)',
|
|
13
|
+
)
|
|
14
|
+
.addHelpText(
|
|
15
|
+
'after',
|
|
16
|
+
buildHelpText({
|
|
17
|
+
context: `Warning: Deleting a segment removes it as a target for future broadcasts,
|
|
18
|
+
but does NOT delete the contacts within it.
|
|
19
|
+
|
|
20
|
+
Non-interactive: --yes is required to confirm deletion when stdin/stdout is not a TTY.`,
|
|
21
|
+
output: ` {"object":"segment","id":"<uuid>","deleted":true}`,
|
|
22
|
+
errorCodes: ['auth_error', 'confirmation_required', 'delete_error'],
|
|
23
|
+
examples: [
|
|
24
|
+
'resend segments delete 78261eea-8f8b-4381-83c6-79fa7120f1cf --yes',
|
|
25
|
+
'resend segments delete 78261eea-8f8b-4381-83c6-79fa7120f1cf --yes --json',
|
|
26
|
+
],
|
|
27
|
+
}),
|
|
28
|
+
)
|
|
29
|
+
.action(async (id, opts, cmd) => {
|
|
30
|
+
const globalOpts = cmd.optsWithGlobals() as GlobalOpts;
|
|
31
|
+
await runDelete(
|
|
32
|
+
id,
|
|
33
|
+
!!opts.yes,
|
|
34
|
+
{
|
|
35
|
+
confirmMessage: `Delete segment ${id}?\nContacts will not be deleted, but broadcasts targeting this segment will no longer work.`,
|
|
36
|
+
spinner: {
|
|
37
|
+
loading: 'Deleting segment...',
|
|
38
|
+
success: 'Segment deleted',
|
|
39
|
+
fail: 'Failed to delete segment',
|
|
40
|
+
},
|
|
41
|
+
object: 'segment',
|
|
42
|
+
successMsg: 'Segment deleted.',
|
|
43
|
+
sdkCall: (resend) => resend.segments.remove(id),
|
|
44
|
+
},
|
|
45
|
+
globalOpts,
|
|
46
|
+
);
|
|
47
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Command } from '@commander-js/extra-typings';
|
|
2
|
+
import { runGet } from '../../lib/actions';
|
|
3
|
+
import type { GlobalOpts } from '../../lib/client';
|
|
4
|
+
import { buildHelpText } from '../../lib/help-text';
|
|
5
|
+
|
|
6
|
+
export const getSegmentCommand = new Command('get')
|
|
7
|
+
.description('Retrieve a segment by ID')
|
|
8
|
+
.argument('<id>', 'Segment UUID')
|
|
9
|
+
.addHelpText(
|
|
10
|
+
'after',
|
|
11
|
+
buildHelpText({
|
|
12
|
+
output: ` {"object":"segment","id":"<uuid>","name":"<name>","created_at":"<iso-date>"}`,
|
|
13
|
+
errorCodes: ['auth_error', 'fetch_error'],
|
|
14
|
+
examples: [
|
|
15
|
+
'resend segments get 78261eea-8f8b-4381-83c6-79fa7120f1cf',
|
|
16
|
+
'resend segments get 78261eea-8f8b-4381-83c6-79fa7120f1cf --json',
|
|
17
|
+
],
|
|
18
|
+
}),
|
|
19
|
+
)
|
|
20
|
+
.action(async (id, _opts, cmd) => {
|
|
21
|
+
const globalOpts = cmd.optsWithGlobals() as GlobalOpts;
|
|
22
|
+
await runGet(
|
|
23
|
+
{
|
|
24
|
+
spinner: {
|
|
25
|
+
loading: 'Fetching segment...',
|
|
26
|
+
success: 'Segment fetched',
|
|
27
|
+
fail: 'Failed to fetch segment',
|
|
28
|
+
},
|
|
29
|
+
sdkCall: (resend) => resend.segments.get(id),
|
|
30
|
+
onInteractive: (data) => {
|
|
31
|
+
console.log(`\n${data.name}`);
|
|
32
|
+
console.log(`ID: ${data.id}`);
|
|
33
|
+
console.log(`Created: ${data.created_at}`);
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
globalOpts,
|
|
37
|
+
);
|
|
38
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Command } from '@commander-js/extra-typings';
|
|
2
|
+
import { buildHelpText } from '../../lib/help-text';
|
|
3
|
+
import { createSegmentCommand } from './create';
|
|
4
|
+
import { deleteSegmentCommand } from './delete';
|
|
5
|
+
import { getSegmentCommand } from './get';
|
|
6
|
+
import { listSegmentsCommand } from './list';
|
|
7
|
+
|
|
8
|
+
export const segmentsCommand = new Command('segments')
|
|
9
|
+
.description(
|
|
10
|
+
'Manage segments — named groups of contacts used to target broadcasts',
|
|
11
|
+
)
|
|
12
|
+
.addHelpText(
|
|
13
|
+
'after',
|
|
14
|
+
buildHelpText({
|
|
15
|
+
context: `Segments are the modern replacement for Audiences (deprecated).
|
|
16
|
+
A segment is a named group of contacts. Broadcasts target segments via segment_id.
|
|
17
|
+
Contacts can belong to multiple segments.
|
|
18
|
+
|
|
19
|
+
Segment membership is managed through the contacts namespace:
|
|
20
|
+
resend contacts add-segment <contactId> --segment-id <segmentId>
|
|
21
|
+
resend contacts remove-segment <contactId> <segmentId>
|
|
22
|
+
resend contacts segments <contactId>
|
|
23
|
+
|
|
24
|
+
There is no "update" endpoint — to rename a segment, delete it and recreate.`,
|
|
25
|
+
examples: [
|
|
26
|
+
'resend segments list',
|
|
27
|
+
'resend segments create --name "Newsletter Subscribers"',
|
|
28
|
+
'resend segments get 78261eea-8f8b-4381-83c6-79fa7120f1cf',
|
|
29
|
+
'resend segments delete 78261eea-8f8b-4381-83c6-79fa7120f1cf --yes',
|
|
30
|
+
],
|
|
31
|
+
}),
|
|
32
|
+
)
|
|
33
|
+
.addCommand(createSegmentCommand)
|
|
34
|
+
.addCommand(getSegmentCommand)
|
|
35
|
+
.addCommand(listSegmentsCommand, { isDefault: true })
|
|
36
|
+
.addCommand(deleteSegmentCommand);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Command } from '@commander-js/extra-typings';
|
|
2
|
+
import { runList } from '../../lib/actions';
|
|
3
|
+
import type { GlobalOpts } from '../../lib/client';
|
|
4
|
+
import { buildHelpText } from '../../lib/help-text';
|
|
5
|
+
import {
|
|
6
|
+
buildPaginationOpts,
|
|
7
|
+
parseLimitOpt,
|
|
8
|
+
printPaginationHint,
|
|
9
|
+
} from '../../lib/pagination';
|
|
10
|
+
import { renderSegmentsTable } from './utils';
|
|
11
|
+
|
|
12
|
+
export const listSegmentsCommand = new Command('list')
|
|
13
|
+
.alias('ls')
|
|
14
|
+
.description('List all segments')
|
|
15
|
+
.option('--limit <n>', 'Maximum number of segments to return (1-100)', '10')
|
|
16
|
+
.option('--after <cursor>', 'Return segments after this cursor (next page)')
|
|
17
|
+
.option(
|
|
18
|
+
'--before <cursor>',
|
|
19
|
+
'Return segments before this cursor (previous page)',
|
|
20
|
+
)
|
|
21
|
+
.addHelpText(
|
|
22
|
+
'after',
|
|
23
|
+
buildHelpText({
|
|
24
|
+
context: `Pagination: use --after or --before with a segment ID as the cursor.
|
|
25
|
+
Only one of --after or --before may be used at a time.
|
|
26
|
+
The response includes has_more: true when additional pages exist.
|
|
27
|
+
|
|
28
|
+
Use "resend segments list" to discover segment IDs for use with broadcasts
|
|
29
|
+
or "resend contacts add-segment".`,
|
|
30
|
+
output: ` {"object":"list","data":[{"id":"<uuid>","name":"<name>","created_at":"<iso-date>"}],"has_more":false}`,
|
|
31
|
+
errorCodes: ['auth_error', 'invalid_limit', 'list_error'],
|
|
32
|
+
examples: [
|
|
33
|
+
'resend segments list',
|
|
34
|
+
'resend segments list --limit 25 --json',
|
|
35
|
+
'resend segments list --after 78261eea-8f8b-4381-83c6-79fa7120f1cf --json',
|
|
36
|
+
],
|
|
37
|
+
}),
|
|
38
|
+
)
|
|
39
|
+
.action(async (opts, cmd) => {
|
|
40
|
+
const globalOpts = cmd.optsWithGlobals() as GlobalOpts;
|
|
41
|
+
const limit = parseLimitOpt(opts.limit, globalOpts);
|
|
42
|
+
const paginationOpts = buildPaginationOpts(limit, opts.after, opts.before);
|
|
43
|
+
await runList(
|
|
44
|
+
{
|
|
45
|
+
spinner: {
|
|
46
|
+
loading: 'Fetching segments...',
|
|
47
|
+
success: 'Segments fetched',
|
|
48
|
+
fail: 'Failed to list segments',
|
|
49
|
+
},
|
|
50
|
+
sdkCall: (resend) => resend.segments.list(paginationOpts),
|
|
51
|
+
onInteractive: (list) => {
|
|
52
|
+
console.log(renderSegmentsTable(list.data));
|
|
53
|
+
printPaginationHint(list);
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
globalOpts,
|
|
57
|
+
);
|
|
58
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Segment } from 'resend';
|
|
2
|
+
import { renderTable } from '../../lib/table';
|
|
3
|
+
|
|
4
|
+
export function renderSegmentsTable(segments: Segment[]): string {
|
|
5
|
+
const rows = segments.map((s) => [s.name, s.id, s.created_at]);
|
|
6
|
+
return renderTable(['Name', 'ID', 'Created'], rows, '(no segments)');
|
|
7
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Command } from '@commander-js/extra-typings';
|
|
2
|
+
import { listCommand } from './list';
|
|
3
|
+
import { removeCommand } from './remove';
|
|
4
|
+
import { switchCommand } from './switch';
|
|
5
|
+
|
|
6
|
+
export const teamsCommand = new Command('teams')
|
|
7
|
+
.description('Manage team profiles for multiple API keys')
|
|
8
|
+
.addCommand(listCommand)
|
|
9
|
+
.addCommand(switchCommand)
|
|
10
|
+
.addCommand(removeCommand);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Command } from '@commander-js/extra-typings';
|
|
2
|
+
import type { GlobalOpts } from '../../lib/client';
|
|
3
|
+
import { listTeams } from '../../lib/config';
|
|
4
|
+
import { outputResult } from '../../lib/output';
|
|
5
|
+
import { isInteractive } from '../../lib/tty';
|
|
6
|
+
|
|
7
|
+
export const listCommand = new Command('list')
|
|
8
|
+
.description('List all team profiles')
|
|
9
|
+
.action((_opts, cmd) => {
|
|
10
|
+
const globalOpts = cmd.optsWithGlobals() as GlobalOpts;
|
|
11
|
+
const teams = listTeams();
|
|
12
|
+
|
|
13
|
+
if (globalOpts.json) {
|
|
14
|
+
outputResult({ teams }, { json: true });
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (teams.length === 0) {
|
|
19
|
+
console.log('No teams configured. Run: resend login');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (isInteractive()) {
|
|
24
|
+
console.log('\n Teams\n');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
for (const team of teams) {
|
|
28
|
+
const marker = team.active ? ' (active)' : '';
|
|
29
|
+
console.log(` ${team.active ? '▸' : ' '} ${team.name}${marker}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (isInteractive()) {
|
|
33
|
+
console.log('');
|
|
34
|
+
}
|
|
35
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import { Command } from '@commander-js/extra-typings';
|
|
3
|
+
import type { GlobalOpts } from '../../lib/client';
|
|
4
|
+
import { listTeams, removeTeam } from '../../lib/config';
|
|
5
|
+
import { errorMessage, outputError, outputResult } from '../../lib/output';
|
|
6
|
+
import { cancelAndExit } from '../../lib/prompts';
|
|
7
|
+
import { isInteractive } from '../../lib/tty';
|
|
8
|
+
|
|
9
|
+
export const removeCommand = new Command('remove')
|
|
10
|
+
.description('Remove a team profile')
|
|
11
|
+
.argument('[name]', 'Team name to remove')
|
|
12
|
+
.action(async (name, _opts, cmd) => {
|
|
13
|
+
const globalOpts = cmd.optsWithGlobals() as GlobalOpts;
|
|
14
|
+
|
|
15
|
+
let teamName = name;
|
|
16
|
+
|
|
17
|
+
if (!teamName) {
|
|
18
|
+
if (!isInteractive()) {
|
|
19
|
+
outputError(
|
|
20
|
+
{
|
|
21
|
+
message:
|
|
22
|
+
'Missing team name. Provide a team name in non-interactive mode.',
|
|
23
|
+
code: 'missing_name',
|
|
24
|
+
},
|
|
25
|
+
{ json: globalOpts.json },
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const teams = listTeams();
|
|
30
|
+
if (teams.length === 0) {
|
|
31
|
+
outputError(
|
|
32
|
+
{
|
|
33
|
+
message: 'No teams configured. Run `resend login` first.',
|
|
34
|
+
code: 'no_teams',
|
|
35
|
+
},
|
|
36
|
+
{ json: globalOpts.json },
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const choice = await p.select({
|
|
41
|
+
message: 'Remove which team?',
|
|
42
|
+
options: teams.map((t) => ({
|
|
43
|
+
value: t.name,
|
|
44
|
+
label: t.name,
|
|
45
|
+
hint: t.active ? 'active' : undefined,
|
|
46
|
+
})),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (p.isCancel(choice)) {
|
|
50
|
+
cancelAndExit('Remove cancelled.');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
teamName = choice;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!globalOpts.json && isInteractive()) {
|
|
57
|
+
const confirmed = await p.confirm({
|
|
58
|
+
message: `Remove team '${teamName}' and its API key?`,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (p.isCancel(confirmed) || !confirmed) {
|
|
62
|
+
cancelAndExit('Remove cancelled.');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
removeTeam(teamName);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
outputError(
|
|
70
|
+
{
|
|
71
|
+
message: errorMessage(err, 'Failed to remove team'),
|
|
72
|
+
code: 'remove_failed',
|
|
73
|
+
},
|
|
74
|
+
{ json: globalOpts.json },
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (globalOpts.json) {
|
|
79
|
+
outputResult({ success: true, removed_team: teamName }, { json: true });
|
|
80
|
+
} else if (isInteractive()) {
|
|
81
|
+
console.log(`Team '${teamName}' removed.`);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import { Command } from '@commander-js/extra-typings';
|
|
3
|
+
import type { GlobalOpts } from '../../lib/client';
|
|
4
|
+
import { listTeams, setActiveTeam } from '../../lib/config';
|
|
5
|
+
import { errorMessage, outputError, outputResult } from '../../lib/output';
|
|
6
|
+
import { cancelAndExit } from '../../lib/prompts';
|
|
7
|
+
import { isInteractive } from '../../lib/tty';
|
|
8
|
+
|
|
9
|
+
export const switchCommand = new Command('switch')
|
|
10
|
+
.description('Switch the active team profile')
|
|
11
|
+
.argument('[name]', 'Team name to switch to')
|
|
12
|
+
.action(async (name, _opts, cmd) => {
|
|
13
|
+
const globalOpts = cmd.optsWithGlobals() as GlobalOpts;
|
|
14
|
+
|
|
15
|
+
let teamName = name;
|
|
16
|
+
|
|
17
|
+
if (!teamName) {
|
|
18
|
+
if (!isInteractive()) {
|
|
19
|
+
outputError(
|
|
20
|
+
{
|
|
21
|
+
message:
|
|
22
|
+
'Missing team name. Provide a team name in non-interactive mode.',
|
|
23
|
+
code: 'missing_name',
|
|
24
|
+
},
|
|
25
|
+
{ json: globalOpts.json },
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const teams = listTeams();
|
|
30
|
+
if (teams.length === 0) {
|
|
31
|
+
outputError(
|
|
32
|
+
{
|
|
33
|
+
message: 'No teams configured. Run `resend login` first.',
|
|
34
|
+
code: 'no_teams',
|
|
35
|
+
},
|
|
36
|
+
{ json: globalOpts.json },
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const choice = await p.select({
|
|
41
|
+
message: 'Switch to which team?',
|
|
42
|
+
options: teams.map((t) => ({
|
|
43
|
+
value: t.name,
|
|
44
|
+
label: t.name,
|
|
45
|
+
hint: t.active ? 'active' : undefined,
|
|
46
|
+
})),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (p.isCancel(choice)) {
|
|
50
|
+
cancelAndExit('Switch cancelled.');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
teamName = choice;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
setActiveTeam(teamName);
|
|
58
|
+
} catch (err) {
|
|
59
|
+
outputError(
|
|
60
|
+
{
|
|
61
|
+
message: errorMessage(err, 'Failed to switch team'),
|
|
62
|
+
code: 'switch_failed',
|
|
63
|
+
},
|
|
64
|
+
{ json: globalOpts.json },
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (globalOpts.json) {
|
|
69
|
+
outputResult({ success: true, active_team: teamName }, { json: true });
|
|
70
|
+
} else if (isInteractive()) {
|
|
71
|
+
console.log(`Switched to team '${teamName}'.`);
|
|
72
|
+
}
|
|
73
|
+
});
|