Skip to content

Commit ce6f88a

Browse files
robertombclaudefelipefreitag
authored
fix(templates): resolve alias before opening a template in the dashboard (#411)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Felipe Freitag <felipe.freitag@resend.com>
1 parent b4f0be1 commit ce6f88a

3 files changed

Lines changed: 86 additions & 12 deletions

File tree

‎skills/resend-cli/references/templates.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,4 @@ Same optional flags as `create` (including `--react-email`, `--text-file`, and `
7474

7575
Open a template (or the templates list) in the Resend dashboard.
7676

77-
**Argument:** `[id]` — Template ID (omit to open the list)
77+
**Argument:** `[id|alias]` — Template ID or alias (omit to open the list)

‎src/commands/templates/open.ts‎

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,51 @@
11
import { Command } from '@commander-js/extra-typings';
22
import { openInBrowserOrLog, RESEND_URLS } from '../../lib/browser';
33
import type { GlobalOpts } from '../../lib/client';
4+
import { requireClient } from '../../lib/client';
45
import { buildHelpText } from '../../lib/help-text';
6+
import { withSpinner } from '../../lib/spinner';
7+
8+
const UUID_PATTERN =
9+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
510

611
export const openTemplateCommand = new Command('open')
712
.description('Open a template or the templates list in the Resend dashboard')
8-
.argument('[id]', 'Template ID — omit to open the templates list')
13+
.argument('[id]', 'Template ID or alias — omit to open the templates list')
914
.addHelpText(
1015
'after',
1116
buildHelpText({
1217
context: `Opens the Resend dashboard in your default browser.
13-
With an ID: opens that template's page for editing or viewing.
18+
With an ID or alias: opens that template's page for editing or viewing.
1419
Without an ID: opens the templates list.`,
1520
examples: [
1621
'resend templates open',
1722
'resend templates open 78261eea-8f8b-4381-83c6-79fa7120f1cf',
23+
'resend templates open my-template-alias',
1824
],
1925
}),
2026
)
21-
.action(async (id: string | undefined, _opts, cmd) => {
22-
const url = id ? RESEND_URLS.template(id) : RESEND_URLS.templates;
27+
.action(async (idOrAlias: string | undefined, _opts, cmd) => {
2328
const globalOpts = cmd.optsWithGlobals() as GlobalOpts;
24-
await openInBrowserOrLog(url, globalOpts);
29+
if (!idOrAlias) {
30+
await openInBrowserOrLog(RESEND_URLS.templates, globalOpts);
31+
return;
32+
}
33+
34+
// The dashboard only routes template pages by ID, so an alias would 404.
35+
const id = UUID_PATTERN.test(idOrAlias)
36+
? idOrAlias
37+
: await resolveTemplateId(idOrAlias, globalOpts);
38+
await openInBrowserOrLog(RESEND_URLS.template(id), globalOpts);
2539
});
40+
41+
async function resolveTemplateId(alias: string, globalOpts: GlobalOpts) {
42+
const resend = await requireClient(globalOpts);
43+
const template = await withSpinner(
44+
'Finding template...',
45+
() => resend.templates.get(alias),
46+
'fetch_error',
47+
globalOpts,
48+
{ retryTransient: true },
49+
);
50+
return template.id;
51+
}
Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,39 @@
11
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
22
import { openTemplateCommand } from '../../../src/commands/templates/open';
33
import * as browser from '../../../src/lib/browser';
4+
import {
5+
captureTestEnv,
6+
expectExit1,
7+
mockExitThrow,
8+
setNonInteractive,
9+
setupOutputSpies,
10+
} from '../../helpers';
11+
12+
const templateId = '78261eea-8f8b-4381-83c6-79fa7120f1cf';
13+
14+
const mockGet = vi.fn(async (_id: string) => ({
15+
data: { object: 'template' as const, id: templateId },
16+
error: null as { message: string } | null,
17+
}));
18+
19+
vi.mock('resend', () => ({
20+
Resend: class MockResend {
21+
constructor(public key: string) {}
22+
templates = { get: mockGet };
23+
},
24+
}));
425

526
describe('templates open command', () => {
27+
const restoreEnv = captureTestEnv();
28+
629
beforeEach(() => {
30+
process.env.RESEND_API_KEY = 're_test_key';
731
vi.spyOn(browser, 'openInBrowserOrLog').mockResolvedValue();
32+
mockGet.mockClear();
833
});
934

1035
afterEach(() => {
36+
restoreEnv();
1137
vi.restoreAllMocks();
1238
});
1339

@@ -21,16 +47,38 @@ describe('templates open command', () => {
2147
);
2248
});
2349

24-
it('with id opens template URL', async () => {
25-
await openTemplateCommand.parseAsync(
26-
['78261eea-8f8b-4381-83c6-79fa7120f1cf'],
27-
{ from: 'user' },
50+
it('with id opens template URL without an API call', async () => {
51+
await openTemplateCommand.parseAsync([templateId], { from: 'user' });
52+
53+
expect(mockGet).not.toHaveBeenCalled();
54+
expect(browser.openInBrowserOrLog).toHaveBeenCalledWith(
55+
browser.RESEND_URLS.template(templateId),
56+
expect.any(Object),
2857
);
58+
});
2959

30-
expect(browser.openInBrowserOrLog).toHaveBeenCalledTimes(1);
60+
it('with alias opens the URL of the template it resolves to', async () => {
61+
await openTemplateCommand.parseAsync(['welcome-email'], { from: 'user' });
62+
63+
expect(mockGet).toHaveBeenCalledWith('welcome-email');
3164
expect(browser.openInBrowserOrLog).toHaveBeenCalledWith(
32-
browser.RESEND_URLS.template('78261eea-8f8b-4381-83c6-79fa7120f1cf'),
65+
browser.RESEND_URLS.template(templateId),
3366
expect.any(Object),
3467
);
3568
});
69+
70+
it('with unknown alias exits without opening the browser', async () => {
71+
setNonInteractive();
72+
setupOutputSpies();
73+
mockExitThrow();
74+
mockGet.mockResolvedValueOnce({
75+
data: null as never,
76+
error: { message: 'Template not found' },
77+
});
78+
79+
await expectExit1(() =>
80+
openTemplateCommand.parseAsync(['missing-alias'], { from: 'user' }),
81+
);
82+
expect(browser.openInBrowserOrLog).not.toHaveBeenCalled();
83+
});
3684
});

0 commit comments

Comments
 (0)