Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "resend-cli",
"version": "1.11.0",
"version": "2.0.0",
"description": "The official CLI for Resend",
"license": "MIT",
"repository": {
Expand Down
2 changes: 1 addition & 1 deletion skills/resend-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ description: >
license: MIT
metadata:
author: resend
version: "1.12.0"
version: "2.0.0"
homepage: https://resend.com/docs/cli-agents
source: https://github.com/resend/resend-cli
openclaw:
Expand Down
9 changes: 0 additions & 9 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import { eventsCommand } from './commands/events/index';
import { logsCommand } from './commands/logs/index';
import { openCommand } from './commands/open';
import { segmentsCommand } from './commands/segments/index';
import { teamsDeprecatedCommand } from './commands/teams-deprecated';
import { templatesCommand } from './commands/templates/index';
import { topicsCommand } from './commands/topics/index';
import { updateCommand } from './commands/update';
Expand Down Expand Up @@ -58,7 +57,6 @@ const program = new Command()
)
.option('--api-key <key>', 'Resend API key (overrides env/config)')
.option('-p, --profile <name>', 'Profile to use (overrides RESEND_PROFILE)')
.option('--team <name>', 'Deprecated: use --profile instead')
.option('--json', 'Force JSON output')
.option('-q, --quiet', 'Suppress spinners and status output (implies --json)')
.option(
Expand Down Expand Up @@ -149,7 +147,6 @@ ${pc.gray('Examples:')}
.addCommand(openCommand)
.addCommand(docsCommand)
.addCommand(updateCommand)
.addCommand(teamsDeprecatedCommand)
.addCommand(listCommandsCommand)
.addCommand(completionCommand);

Expand All @@ -167,12 +164,6 @@ telemetryCommand

program.addCommand(telemetryCommand, { hidden: true });

// Hide the deprecated --team option from help
const teamOption = program.options.find((o) => o.long === '--team');
if (teamOption) {
teamOption.hidden = true;
}

program
.parseAsync()
.then(() => {
Expand Down
3 changes: 1 addition & 2 deletions src/commands/auth/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,7 @@ export const loginCommand = new Command('login')
);
}

let profileName =
(globalOpts.profile ?? globalOpts.team)?.trim() || undefined;
let profileName = globalOpts.profile?.trim() || undefined;

if (profileName) {
const profileError = validateProfileName(profileName);
Expand Down
2 changes: 1 addition & 1 deletion src/commands/auth/logout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ If no credentials file exists, exits cleanly with no error.`,
return;
}

const profileFlag = globalOpts.profile ?? globalOpts.team;
const profileFlag = globalOpts.profile;
const logoutAll = !profileFlag;
const profileLabel = profileFlag || resolveProfileName();

Expand Down
3 changes: 1 addition & 2 deletions src/commands/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,8 +238,7 @@ export const doctorCommand = new Command('doctor')
...(!usingSecure &&
process.env.RESEND_CREDENTIAL_STORE !== 'file' &&
(creds?.storage === 'secure_storage' ||
process.env.RESEND_CREDENTIAL_STORE === 'secure_storage' ||
process.env.RESEND_CREDENTIAL_STORE === 'keychain')
process.env.RESEND_CREDENTIAL_STORE === 'secure_storage')
? {
detail:
'Secure backend unavailable despite secure storage preference — falling back to plaintext',
Expand Down
49 changes: 0 additions & 49 deletions src/commands/teams-deprecated.ts

This file was deleted.

5 changes: 2 additions & 3 deletions src/commands/whoami.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Shows which profile is active and where the API key comes from.`,
)
.action(async (_opts, cmd) => {
const globalOpts = cmd.optsWithGlobals() as GlobalOpts;
const profileFlag = globalOpts.profile ?? globalOpts.team;
const profileFlag = globalOpts.profile;
const resolved = await resolveApiKeyAsync(globalOpts.apiKey, profileFlag);

if (!resolved) {
Expand All @@ -40,8 +40,7 @@ Shows which profile is active and where the API key comes from.`,
: resolveProfileName(profileFlag);
const profiles = listProfiles();
const profileExists = profiles.some((p) => p.name === requestedProfile);
const explicitProfile =
profileFlag || process.env.RESEND_PROFILE || process.env.RESEND_TEAM;
const explicitProfile = profileFlag || process.env.RESEND_PROFILE;
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Check --profile presence with !== undefined instead of truthiness so empty-string values are treated as explicitly provided.

(Based on your team's feedback about Commander.js option presence checks.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/commands/whoami.ts, line 43:

<comment>Check `--profile` presence with `!== undefined` instead of truthiness so empty-string values are treated as explicitly provided.

(Based on your team's feedback about Commander.js option presence checks.) </comment>

<file context>
@@ -40,8 +40,7 @@ Shows which profile is active and where the API key comes from.`,
       const profileExists = profiles.some((p) => p.name === requestedProfile);
-      const explicitProfile =
-        profileFlag || process.env.RESEND_PROFILE || process.env.RESEND_TEAM;
+      const explicitProfile = profileFlag || process.env.RESEND_PROFILE;
 
       // If a specific profile was requested but doesn't exist, show a targeted error
</file context>
Fix with Cubic


// If a specific profile was requested but doesn't exist, show a targeted error
const message =
Expand Down
4 changes: 1 addition & 3 deletions src/lib/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ export type GlobalOpts = {
json?: boolean;
quiet?: boolean;
profile?: string;
/** @deprecated Use `profile` instead */
team?: string;
};

export type RequireClientOpts = {
Expand Down Expand Up @@ -59,7 +57,7 @@ export async function requireClient(
opts: GlobalOpts,
clientOpts?: RequireClientOpts,
): Promise<Resend> {
const profileName = opts.profile ?? opts.team;
const profileName = opts.profile;

try {
const resolved = await resolveApiKeyAsync(opts.apiKey, profileName);
Expand Down
60 changes: 4 additions & 56 deletions src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,6 @@ export type CredentialsFile = {
profiles: Record<string, Profile>;
};

/** @deprecated Use `Profile` instead */
export type TeamProfile = Profile;

export function getConfigDir(): string {
if (process.env.XDG_CONFIG_HOME) {
return join(process.env.XDG_CONFIG_HOME, 'resend');
Expand All @@ -54,30 +51,13 @@ export function getCredentialsPath(): string {
export function readCredentials(): CredentialsFile | null {
try {
const data = JSON.parse(readFileSync(getCredentialsPath(), 'utf-8'));
// Support legacy format: { api_key: "re_xxx" }
if (data.api_key && !data.profiles && !data.teams) {
return {
active_profile: 'default',
profiles: { default: { api_key: data.api_key } },
};
}
// New format: { profiles, active_profile }
if (data.profiles) {
const storage =
data.storage === 'keychain' ? 'secure_storage' : data.storage;
return {
active_profile: data.active_profile ?? 'default',
...(storage ? { storage } : {}),
...(data.storage ? { storage: data.storage } : {}),
profiles: data.profiles,
};
}
// Old format: { teams, active_team }
if (data.teams) {
return {
active_profile: data.active_team ?? 'default',
profiles: data.teams,
};
}
return null;
} catch {
return null;
Expand All @@ -102,8 +82,7 @@ export function resolveProfileName(flagValue?: string): string {
return flagValue;
}

// Check RESEND_PROFILE first, fall back to deprecated RESEND_TEAM
const envProfile = process.env.RESEND_PROFILE || process.env.RESEND_TEAM;
const envProfile = process.env.RESEND_PROFILE;
if (envProfile) {
return envProfile;
}
Expand All @@ -116,9 +95,6 @@ export function resolveProfileName(flagValue?: string): string {
return 'default';
}

/** @deprecated Use `resolveProfileName` instead */
export const resolveTeamName = resolveProfileName;

export function resolveApiKey(
flagValue?: string,
profileName?: string,
Expand Down Expand Up @@ -190,7 +166,7 @@ export function removeApiKey(profileName?: string): string {
if (!existsSync(configPath)) {
throw new Error('No credentials file found.');
}
// Try to delete legacy file
// File exists but is not valid credentials — delete it
unlinkSync(configPath);
return configPath;
}
Expand Down Expand Up @@ -237,9 +213,6 @@ export function setActiveProfile(profileName: string): void {
writeCredentials(creds);
}

/** @deprecated Use `setActiveProfile` instead */
export const setActiveTeam = setActiveProfile;

export function listProfiles(): Array<{ name: string; active: boolean }> {
const creds = readCredentials();
if (!creds) {
Expand All @@ -251,9 +224,6 @@ export function listProfiles(): Array<{ name: string; active: boolean }> {
}));
}

/** @deprecated Use `listProfiles` instead */
export const listTeams = listProfiles;

export function validateProfileName(name: string): string | undefined {
if (!name || name.length === 0) {
return 'Profile name must not be empty';
Expand All @@ -267,9 +237,6 @@ export function validateProfileName(name: string): string | undefined {
return undefined;
}

/** @deprecated Use `validateProfileName` instead */
export const validateTeamName = validateProfileName;

export function renameProfile(oldName: string, newName: string): void {
if (oldName === newName) {
return;
Expand Down Expand Up @@ -324,7 +291,6 @@ export async function resolveApiKeyAsync(
const profile =
profileName ||
process.env.RESEND_PROFILE ||
process.env.RESEND_TEAM ||
creds?.active_profile ||
'default';

Expand All @@ -337,27 +303,10 @@ export async function resolveApiKeyAsync(
}
}

// File-based storage (or unmigrated profile in mixed state)
// File-based storage
if (creds) {
const entry = creds.profiles[profile];
if (entry?.api_key) {
// Auto-migrate: move plaintext key to secure storage if available
const backend = await getCredentialBackend();
if (backend.isSecure) {
try {
await backend.set(SERVICE_NAME, profile, entry.api_key);
creds.profiles[profile] = {
...(entry.permission && { permission: entry.permission }),
};
creds.storage = 'secure_storage';
writeCredentials(creds);
process.stderr.write(
`Notice: API key for profile "${profile}" has been moved to ${backend.name}\n`,
);
} catch {
// Non-fatal — plaintext key still works
}
}
return {
key: entry.api_key,
source: 'config',
Expand Down Expand Up @@ -417,7 +366,6 @@ export async function removeApiKeyAsync(profileName?: string): Promise<string> {
const profile =
profileName ||
process.env.RESEND_PROFILE ||
process.env.RESEND_TEAM ||
creds?.active_profile ||
'default';

Expand Down
4 changes: 2 additions & 2 deletions src/lib/credential-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ export async function getCredentialBackend(): Promise<CredentialBackend> {
return cachedBackend;
}

if (override === 'secure_storage' || override === 'keychain') {
if (override === 'secure_storage') {
const backend = await getOsBackend();
if (backend) {
cachedBackend = backend;
return cachedBackend;
}
// Fall through to file if keychain forced but unavailable
// Fall through to file if secure storage forced but unavailable
}

// Auto-detect: try OS backend first
Expand Down
24 changes: 0 additions & 24 deletions tests/commands/auth/login.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,30 +263,6 @@ describe('login command', () => {
expect(data.profiles.staging.api_key).toBe('re_staging_key_123');
});

it('deprecated --team alias works like --profile', async () => {
setupOutputSpies();

const { Command } = await import('@commander-js/extra-typings');
const { loginCommand } = await import('../../../src/commands/auth/login');
const program = new Command()
.option('--profile <name>')
.option('--team <name>')
.option('--json')
.option('--api-key <key>')
.option('-q, --quiet')
.addCommand(loginCommand);

await program.parseAsync(
['login', '--key', 're_team_alias_key_123', '--team', 'legacy'],
{ from: 'user' },
);

const configPath = join(tmpDir, 'resend', 'credentials.json');
const data = JSON.parse(readFileSync(configPath, 'utf-8'));
expect(data.active_profile).toBe('legacy');
expect(data.profiles.legacy.api_key).toBe('re_team_alias_key_123');
});

it('rejects invalid profile name with invalid_profile_name', async () => {
setNonInteractive();
errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
Expand Down
1 change: 0 additions & 1 deletion tests/commands/whoami.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ describe('whoami command', () => {
process.env.XDG_CONFIG_HOME = tmpDir;
delete process.env.RESEND_API_KEY;
delete process.env.RESEND_PROFILE;
delete process.env.RESEND_TEAM;
});

afterEach(() => {
Expand Down
6 changes: 3 additions & 3 deletions tests/lib/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,16 +72,16 @@ describe('createClient', () => {
writeFileSync(
join(configDir, 'credentials.json'),
JSON.stringify({
active_team: 'default',
teams: {
active_profile: 'default',
profiles: {
default: { api_key: 're_default_key' },
staging: { api_key: 're_staging_key' },
},
}),
);

const { createClient } = await import('../../src/lib/client');
// Should not throw — resolves staging team's key
// Should not throw — resolves staging profile's key
const client = await createClient(undefined, 'staging');
expect(client).toBeInstanceOf(Resend);

Expand Down
Loading