superacli 1.1.5 → 1.1.6
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/__tests__/resend-plugin.test.js +22 -1
- package/package.json +1 -1
- package/plugins/plugins.json +2 -2
- package/plugins/resend/plugin.json +279 -2
- package/plugins/resend/skills/quickstart/SKILL.md +32 -13
- package/temp_resend_cli/repo/.github/scripts/pr-title-check.js +34 -0
- package/temp_resend_cli/repo/.github/workflows/ci.yml +67 -0
- package/temp_resend_cli/repo/.github/workflows/post-release.yml +51 -0
- package/temp_resend_cli/repo/.github/workflows/pr-title-check.yml +13 -0
- package/temp_resend_cli/repo/.github/workflows/release.yml +175 -0
- package/temp_resend_cli/repo/.github/workflows/test-install-unix.yml +34 -0
- package/temp_resend_cli/repo/.github/workflows/test-install-windows.yml +48 -0
- package/temp_resend_cli/repo/CHANGELOG.md +31 -0
- package/temp_resend_cli/repo/LICENSE +21 -0
- package/temp_resend_cli/repo/README.md +450 -0
- package/temp_resend_cli/repo/biome.json +36 -0
- package/temp_resend_cli/repo/install.ps1 +141 -0
- package/temp_resend_cli/repo/install.sh +301 -0
- package/temp_resend_cli/repo/package.json +61 -0
- package/temp_resend_cli/repo/pnpm-lock.yaml +2439 -0
- package/temp_resend_cli/repo/renovate.json +4 -0
- package/temp_resend_cli/repo/src/cli.ts +98 -0
- package/temp_resend_cli/repo/src/commands/api-keys/create.ts +114 -0
- package/temp_resend_cli/repo/src/commands/api-keys/delete.ts +47 -0
- package/temp_resend_cli/repo/src/commands/api-keys/index.ts +26 -0
- package/temp_resend_cli/repo/src/commands/api-keys/list.ts +35 -0
- package/temp_resend_cli/repo/src/commands/api-keys/utils.ts +8 -0
- package/temp_resend_cli/repo/src/commands/auth/index.ts +20 -0
- package/temp_resend_cli/repo/src/commands/auth/login.ts +234 -0
- package/temp_resend_cli/repo/src/commands/auth/logout.ts +105 -0
- package/temp_resend_cli/repo/src/commands/broadcasts/create.ts +196 -0
- package/temp_resend_cli/repo/src/commands/broadcasts/delete.ts +46 -0
- package/temp_resend_cli/repo/src/commands/broadcasts/get.ts +59 -0
- package/temp_resend_cli/repo/src/commands/broadcasts/index.ts +43 -0
- package/temp_resend_cli/repo/src/commands/broadcasts/list.ts +60 -0
- package/temp_resend_cli/repo/src/commands/broadcasts/send.ts +56 -0
- package/temp_resend_cli/repo/src/commands/broadcasts/update.ts +95 -0
- package/temp_resend_cli/repo/src/commands/broadcasts/utils.ts +35 -0
- package/temp_resend_cli/repo/src/commands/contact-properties/create.ts +118 -0
- package/temp_resend_cli/repo/src/commands/contact-properties/delete.ts +48 -0
- package/temp_resend_cli/repo/src/commands/contact-properties/get.ts +46 -0
- package/temp_resend_cli/repo/src/commands/contact-properties/index.ts +48 -0
- package/temp_resend_cli/repo/src/commands/contact-properties/list.ts +68 -0
- package/temp_resend_cli/repo/src/commands/contact-properties/update.ts +88 -0
- package/temp_resend_cli/repo/src/commands/contact-properties/utils.ts +17 -0
- package/temp_resend_cli/repo/src/commands/contacts/add-segment.ts +78 -0
- package/temp_resend_cli/repo/src/commands/contacts/create.ts +122 -0
- package/temp_resend_cli/repo/src/commands/contacts/delete.ts +49 -0
- package/temp_resend_cli/repo/src/commands/contacts/get.ts +53 -0
- package/temp_resend_cli/repo/src/commands/contacts/index.ts +58 -0
- package/temp_resend_cli/repo/src/commands/contacts/list.ts +57 -0
- package/temp_resend_cli/repo/src/commands/contacts/remove-segment.ts +48 -0
- package/temp_resend_cli/repo/src/commands/contacts/segments.ts +39 -0
- package/temp_resend_cli/repo/src/commands/contacts/topics.ts +45 -0
- package/temp_resend_cli/repo/src/commands/contacts/update-topics.ts +90 -0
- package/temp_resend_cli/repo/src/commands/contacts/update.ts +77 -0
- package/temp_resend_cli/repo/src/commands/contacts/utils.ts +119 -0
- package/temp_resend_cli/repo/src/commands/doctor.ts +216 -0
- package/temp_resend_cli/repo/src/commands/domains/create.ts +83 -0
- package/temp_resend_cli/repo/src/commands/domains/delete.ts +42 -0
- package/temp_resend_cli/repo/src/commands/domains/get.ts +47 -0
- package/temp_resend_cli/repo/src/commands/domains/index.ts +35 -0
- package/temp_resend_cli/repo/src/commands/domains/list.ts +53 -0
- package/temp_resend_cli/repo/src/commands/domains/update.ts +75 -0
- package/temp_resend_cli/repo/src/commands/domains/utils.ts +44 -0
- package/temp_resend_cli/repo/src/commands/domains/verify.ts +38 -0
- package/temp_resend_cli/repo/src/commands/emails/batch.ts +140 -0
- package/temp_resend_cli/repo/src/commands/emails/get.ts +44 -0
- package/temp_resend_cli/repo/src/commands/emails/index.ts +30 -0
- package/temp_resend_cli/repo/src/commands/emails/list.ts +84 -0
- package/temp_resend_cli/repo/src/commands/emails/receiving/attachment.ts +55 -0
- package/temp_resend_cli/repo/src/commands/emails/receiving/attachments.ts +68 -0
- package/temp_resend_cli/repo/src/commands/emails/receiving/get.ts +58 -0
- package/temp_resend_cli/repo/src/commands/emails/receiving/index.ts +28 -0
- package/temp_resend_cli/repo/src/commands/emails/receiving/list.ts +59 -0
- package/temp_resend_cli/repo/src/commands/emails/receiving/utils.ts +38 -0
- package/temp_resend_cli/repo/src/commands/emails/send.ts +189 -0
- package/temp_resend_cli/repo/src/commands/open.ts +27 -0
- package/temp_resend_cli/repo/src/commands/segments/create.ts +50 -0
- package/temp_resend_cli/repo/src/commands/segments/delete.ts +47 -0
- package/temp_resend_cli/repo/src/commands/segments/get.ts +38 -0
- package/temp_resend_cli/repo/src/commands/segments/index.ts +36 -0
- package/temp_resend_cli/repo/src/commands/segments/list.ts +58 -0
- package/temp_resend_cli/repo/src/commands/segments/utils.ts +7 -0
- package/temp_resend_cli/repo/src/commands/teams/index.ts +10 -0
- package/temp_resend_cli/repo/src/commands/teams/list.ts +35 -0
- package/temp_resend_cli/repo/src/commands/teams/remove.ts +86 -0
- package/temp_resend_cli/repo/src/commands/teams/switch.ts +76 -0
- package/temp_resend_cli/repo/src/commands/topics/create.ts +73 -0
- package/temp_resend_cli/repo/src/commands/topics/delete.ts +47 -0
- package/temp_resend_cli/repo/src/commands/topics/get.ts +42 -0
- package/temp_resend_cli/repo/src/commands/topics/index.ts +42 -0
- package/temp_resend_cli/repo/src/commands/topics/list.ts +34 -0
- package/temp_resend_cli/repo/src/commands/topics/update.ts +59 -0
- package/temp_resend_cli/repo/src/commands/topics/utils.ts +16 -0
- package/temp_resend_cli/repo/src/commands/webhooks/create.ts +128 -0
- package/temp_resend_cli/repo/src/commands/webhooks/delete.ts +49 -0
- package/temp_resend_cli/repo/src/commands/webhooks/get.ts +42 -0
- package/temp_resend_cli/repo/src/commands/webhooks/index.ts +42 -0
- package/temp_resend_cli/repo/src/commands/webhooks/list.ts +55 -0
- package/temp_resend_cli/repo/src/commands/webhooks/listen.ts +379 -0
- package/temp_resend_cli/repo/src/commands/webhooks/update.ts +83 -0
- package/temp_resend_cli/repo/src/commands/webhooks/utils.ts +36 -0
- package/temp_resend_cli/repo/src/commands/whoami.ts +71 -0
- package/temp_resend_cli/repo/src/lib/actions.ts +157 -0
- package/temp_resend_cli/repo/src/lib/client.ts +37 -0
- package/temp_resend_cli/repo/src/lib/config.ts +217 -0
- package/temp_resend_cli/repo/src/lib/files.ts +15 -0
- package/temp_resend_cli/repo/src/lib/help-text.ts +38 -0
- package/temp_resend_cli/repo/src/lib/output.ts +56 -0
- package/temp_resend_cli/repo/src/lib/pagination.ts +36 -0
- package/temp_resend_cli/repo/src/lib/prompts.ts +149 -0
- package/temp_resend_cli/repo/src/lib/spinner.ts +100 -0
- package/temp_resend_cli/repo/src/lib/table.ts +57 -0
- package/temp_resend_cli/repo/src/lib/tty.ts +28 -0
- package/temp_resend_cli/repo/src/lib/update-check.ts +169 -0
- package/temp_resend_cli/repo/src/lib/version.ts +4 -0
- package/temp_resend_cli/repo/tests/commands/api-keys/create.test.ts +196 -0
- package/temp_resend_cli/repo/tests/commands/api-keys/delete.test.ts +157 -0
- package/temp_resend_cli/repo/tests/commands/api-keys/list.test.ts +134 -0
- package/temp_resend_cli/repo/tests/commands/auth/login.test.ts +153 -0
- package/temp_resend_cli/repo/tests/commands/auth/logout.test.ts +153 -0
- package/temp_resend_cli/repo/tests/commands/broadcasts/create.test.ts +454 -0
- package/temp_resend_cli/repo/tests/commands/broadcasts/delete.test.ts +183 -0
- package/temp_resend_cli/repo/tests/commands/broadcasts/get.test.ts +147 -0
- package/temp_resend_cli/repo/tests/commands/broadcasts/list.test.ts +199 -0
- package/temp_resend_cli/repo/tests/commands/broadcasts/send.test.ts +162 -0
- package/temp_resend_cli/repo/tests/commands/broadcasts/update.test.ts +288 -0
- package/temp_resend_cli/repo/tests/commands/contact-properties/create.test.ts +251 -0
- package/temp_resend_cli/repo/tests/commands/contact-properties/delete.test.ts +184 -0
- package/temp_resend_cli/repo/tests/commands/contact-properties/get.test.ts +145 -0
- package/temp_resend_cli/repo/tests/commands/contact-properties/list.test.ts +181 -0
- package/temp_resend_cli/repo/tests/commands/contact-properties/update.test.ts +217 -0
- package/temp_resend_cli/repo/tests/commands/contacts/add-segment.test.ts +189 -0
- package/temp_resend_cli/repo/tests/commands/contacts/create.test.ts +271 -0
- package/temp_resend_cli/repo/tests/commands/contacts/delete.test.ts +193 -0
- package/temp_resend_cli/repo/tests/commands/contacts/get.test.ts +149 -0
- package/temp_resend_cli/repo/tests/commands/contacts/list.test.ts +176 -0
- package/temp_resend_cli/repo/tests/commands/contacts/remove-segment.test.ts +167 -0
- package/temp_resend_cli/repo/tests/commands/contacts/segments.test.ts +168 -0
- package/temp_resend_cli/repo/tests/commands/contacts/topics.test.ts +164 -0
- package/temp_resend_cli/repo/tests/commands/contacts/update-topics.test.ts +248 -0
- package/temp_resend_cli/repo/tests/commands/contacts/update.test.ts +206 -0
- package/temp_resend_cli/repo/tests/commands/doctor.test.ts +164 -0
- package/temp_resend_cli/repo/tests/commands/domains/create.test.ts +193 -0
- package/temp_resend_cli/repo/tests/commands/domains/delete.test.ts +157 -0
- package/temp_resend_cli/repo/tests/commands/domains/get.test.ts +138 -0
- package/temp_resend_cli/repo/tests/commands/domains/list.test.ts +165 -0
- package/temp_resend_cli/repo/tests/commands/domains/update.test.ts +224 -0
- package/temp_resend_cli/repo/tests/commands/domains/verify.test.ts +118 -0
- package/temp_resend_cli/repo/tests/commands/emails/batch.test.ts +324 -0
- package/temp_resend_cli/repo/tests/commands/emails/get.test.ts +132 -0
- package/temp_resend_cli/repo/tests/commands/emails/receiving/attachment.test.ts +141 -0
- package/temp_resend_cli/repo/tests/commands/emails/receiving/attachments.test.ts +169 -0
- package/temp_resend_cli/repo/tests/commands/emails/receiving/get.test.ts +141 -0
- package/temp_resend_cli/repo/tests/commands/emails/receiving/list.test.ts +182 -0
- package/temp_resend_cli/repo/tests/commands/emails/send.test.ts +312 -0
- package/temp_resend_cli/repo/tests/commands/segments/create.test.ts +164 -0
- package/temp_resend_cli/repo/tests/commands/segments/delete.test.ts +183 -0
- package/temp_resend_cli/repo/tests/commands/segments/get.test.ts +138 -0
- package/temp_resend_cli/repo/tests/commands/segments/list.test.ts +174 -0
- package/temp_resend_cli/repo/tests/commands/teams/list.test.ts +62 -0
- package/temp_resend_cli/repo/tests/commands/teams/remove.test.ts +110 -0
- package/temp_resend_cli/repo/tests/commands/teams/switch.test.ts +103 -0
- package/temp_resend_cli/repo/tests/commands/topics/create.test.ts +192 -0
- package/temp_resend_cli/repo/tests/commands/topics/delete.test.ts +157 -0
- package/temp_resend_cli/repo/tests/commands/topics/get.test.ts +126 -0
- package/temp_resend_cli/repo/tests/commands/topics/list.test.ts +125 -0
- package/temp_resend_cli/repo/tests/commands/topics/update.test.ts +178 -0
- package/temp_resend_cli/repo/tests/commands/webhooks/create.test.ts +225 -0
- package/temp_resend_cli/repo/tests/commands/webhooks/delete.test.ts +157 -0
- package/temp_resend_cli/repo/tests/commands/webhooks/get.test.ts +126 -0
- package/temp_resend_cli/repo/tests/commands/webhooks/list.test.ts +178 -0
- package/temp_resend_cli/repo/tests/commands/webhooks/update.test.ts +207 -0
- package/temp_resend_cli/repo/tests/commands/whoami.test.ts +98 -0
- package/temp_resend_cli/repo/tests/e2e/smoke.test.ts +93 -0
- package/temp_resend_cli/repo/tests/helpers.ts +86 -0
- package/temp_resend_cli/repo/tests/lib/client.test.ts +71 -0
- package/temp_resend_cli/repo/tests/lib/config.test.ts +451 -0
- package/temp_resend_cli/repo/tests/lib/files.test.ts +73 -0
- package/temp_resend_cli/repo/tests/lib/help-text.test.ts +97 -0
- package/temp_resend_cli/repo/tests/lib/output.test.ts +136 -0
- package/temp_resend_cli/repo/tests/lib/prompts.test.ts +185 -0
- package/temp_resend_cli/repo/tests/lib/spinner.test.ts +166 -0
- package/temp_resend_cli/repo/tests/lib/table.test.ts +63 -0
- package/temp_resend_cli/repo/tests/lib/tty.test.ts +89 -0
- package/temp_resend_cli/repo/tests/lib/update-check.test.ts +179 -0
- package/temp_resend_cli/repo/tsconfig.json +14 -0
- package/temp_resend_cli/repo/vitest.config.e2e.ts +8 -0
- package/temp_resend_cli/repo/vitest.config.ts +10 -0
- package/tests/test-resend-smoke.sh +7 -3
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { GlobalOpts } from './client';
|
|
2
|
+
import { errorMessage, outputError } from './output';
|
|
3
|
+
import { isInteractive, isUnicodeSupported } from './tty';
|
|
4
|
+
|
|
5
|
+
// Status symbols generated via String.fromCodePoint() — never literal Unicode in
|
|
6
|
+
// source — to prevent UTF-8 → Latin-1 corruption when the npm package is bundled.
|
|
7
|
+
const TICK = isUnicodeSupported ? String.fromCodePoint(0x2714) : 'v'; // ✔
|
|
8
|
+
const WARN = isUnicodeSupported ? String.fromCodePoint(0x26a0) : '!'; // ⚠
|
|
9
|
+
const CROSS = isUnicodeSupported ? String.fromCodePoint(0x2717) : 'x'; // ✗
|
|
10
|
+
|
|
11
|
+
// Braille spinner: cycle through U+2800-block dot patterns.
|
|
12
|
+
const SPINNER_FRAMES = [
|
|
13
|
+
'\u2839',
|
|
14
|
+
'\u2838',
|
|
15
|
+
'\u2834',
|
|
16
|
+
'\u2826',
|
|
17
|
+
'\u2807',
|
|
18
|
+
'\u280F',
|
|
19
|
+
'\u2819',
|
|
20
|
+
'\u2839',
|
|
21
|
+
];
|
|
22
|
+
const SPINNER_INTERVAL = 80;
|
|
23
|
+
|
|
24
|
+
type SdkResponse<T> = { data: T | null; error: { message: string } | null };
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Wraps an SDK call with a spinner, unified error handling, and automatic stop/fail.
|
|
28
|
+
* Eliminates the repeated try/catch + spinner boilerplate across all command files.
|
|
29
|
+
*/
|
|
30
|
+
export async function withSpinner<T>(
|
|
31
|
+
messages: { loading: string; success: string; fail: string },
|
|
32
|
+
call: () => Promise<SdkResponse<T>>,
|
|
33
|
+
errorCode: string,
|
|
34
|
+
globalOpts: GlobalOpts,
|
|
35
|
+
): Promise<T> {
|
|
36
|
+
const spinner = createSpinner(messages.loading, globalOpts.quiet);
|
|
37
|
+
try {
|
|
38
|
+
const { data, error } = await call();
|
|
39
|
+
if (error) {
|
|
40
|
+
spinner.fail(messages.fail);
|
|
41
|
+
outputError(
|
|
42
|
+
{ message: error.message, code: errorCode },
|
|
43
|
+
{ json: globalOpts.json },
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
if (data === null) {
|
|
47
|
+
spinner.fail(messages.fail);
|
|
48
|
+
outputError(
|
|
49
|
+
{ message: 'Unexpected empty response', code: errorCode },
|
|
50
|
+
{ json: globalOpts.json },
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
spinner.stop(messages.success);
|
|
54
|
+
return data;
|
|
55
|
+
} catch (err) {
|
|
56
|
+
spinner.fail(messages.fail);
|
|
57
|
+
return outputError(
|
|
58
|
+
{ message: errorMessage(err, 'Unknown error'), code: errorCode },
|
|
59
|
+
{ json: globalOpts.json },
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function createSpinner(message: string, quiet?: boolean) {
|
|
65
|
+
if (quiet || !isInteractive()) {
|
|
66
|
+
return {
|
|
67
|
+
update(_msg: string) {},
|
|
68
|
+
stop(_msg: string) {},
|
|
69
|
+
warn(_msg: string) {},
|
|
70
|
+
fail(_msg: string) {},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const frames = isUnicodeSupported ? SPINNER_FRAMES : ['-', '\\', '|', '/'];
|
|
75
|
+
const interval = SPINNER_INTERVAL;
|
|
76
|
+
let i = 0;
|
|
77
|
+
let text = message;
|
|
78
|
+
|
|
79
|
+
const timer = setInterval(() => {
|
|
80
|
+
process.stderr.write(`\r\x1B[2K ${frames[i++ % frames.length]} ${text}`);
|
|
81
|
+
}, interval);
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
update(msg: string) {
|
|
85
|
+
text = msg;
|
|
86
|
+
},
|
|
87
|
+
stop(msg: string) {
|
|
88
|
+
clearInterval(timer);
|
|
89
|
+
process.stderr.write(`\r\x1B[2K ${TICK} ${msg}\n`);
|
|
90
|
+
},
|
|
91
|
+
warn(msg: string) {
|
|
92
|
+
clearInterval(timer);
|
|
93
|
+
process.stderr.write(`\r\x1B[2K ${WARN} ${msg}\n`);
|
|
94
|
+
},
|
|
95
|
+
fail(msg: string) {
|
|
96
|
+
clearInterval(timer);
|
|
97
|
+
process.stderr.write(`\r\x1B[2K ${CROSS} ${msg}\n`);
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { isUnicodeSupported } from './tty';
|
|
2
|
+
|
|
3
|
+
// All box-drawing characters generated via String.fromCodePoint() — never literal
|
|
4
|
+
// Unicode in source — to prevent UTF-8 → Latin-1 corruption in npm bundles.
|
|
5
|
+
const BOX = isUnicodeSupported
|
|
6
|
+
? {
|
|
7
|
+
h: String.fromCodePoint(0x2500), // ─
|
|
8
|
+
v: String.fromCodePoint(0x2502), // │
|
|
9
|
+
tl: String.fromCodePoint(0x250c), // ┌
|
|
10
|
+
tr: String.fromCodePoint(0x2510), // ┐
|
|
11
|
+
bl: String.fromCodePoint(0x2514), // └
|
|
12
|
+
br: String.fromCodePoint(0x2518), // ┘
|
|
13
|
+
lm: String.fromCodePoint(0x251c), // ├
|
|
14
|
+
rm: String.fromCodePoint(0x2524), // ┤
|
|
15
|
+
tm: String.fromCodePoint(0x252c), // ┬
|
|
16
|
+
bm: String.fromCodePoint(0x2534), // ┴
|
|
17
|
+
mm: String.fromCodePoint(0x253c), // ┼
|
|
18
|
+
}
|
|
19
|
+
: {
|
|
20
|
+
h: '-',
|
|
21
|
+
v: '|',
|
|
22
|
+
tl: '+',
|
|
23
|
+
tr: '+',
|
|
24
|
+
bl: '+',
|
|
25
|
+
br: '+',
|
|
26
|
+
lm: '+',
|
|
27
|
+
rm: '+',
|
|
28
|
+
tm: '+',
|
|
29
|
+
bm: '+',
|
|
30
|
+
mm: '+',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function renderTable(
|
|
34
|
+
headers: string[],
|
|
35
|
+
rows: string[][],
|
|
36
|
+
emptyMessage = '(no results)',
|
|
37
|
+
): string {
|
|
38
|
+
if (rows.length === 0) {
|
|
39
|
+
return emptyMessage;
|
|
40
|
+
}
|
|
41
|
+
const widths = headers.map((h, i) =>
|
|
42
|
+
Math.max(h.length, ...rows.map((r) => r[i].length)),
|
|
43
|
+
);
|
|
44
|
+
const top =
|
|
45
|
+
BOX.tl + widths.map((w) => BOX.h.repeat(w + 2)).join(BOX.tm) + BOX.tr;
|
|
46
|
+
const mid =
|
|
47
|
+
BOX.lm + widths.map((w) => BOX.h.repeat(w + 2)).join(BOX.mm) + BOX.rm;
|
|
48
|
+
const bot =
|
|
49
|
+
BOX.bl + widths.map((w) => BOX.h.repeat(w + 2)).join(BOX.bm) + BOX.br;
|
|
50
|
+
const row = (cells: string[]) =>
|
|
51
|
+
BOX.v +
|
|
52
|
+
' ' +
|
|
53
|
+
cells.map((c, i) => c.padEnd(widths[i])).join(` ${BOX.v} `) +
|
|
54
|
+
' ' +
|
|
55
|
+
BOX.v;
|
|
56
|
+
return [top, row(headers), mid, ...rows.map(row), bot].join('\n');
|
|
57
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export function isInteractive(): boolean {
|
|
2
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
3
|
+
return false;
|
|
4
|
+
}
|
|
5
|
+
if (process.env.CI === 'true' || process.env.CI === '1') {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
if (process.env.GITHUB_ACTIONS) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
if (process.env.TERM === 'dumb') {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* True on macOS/Linux and on Windows terminals that support Unicode
|
|
19
|
+
* (Windows Terminal, VS Code integrated terminal).
|
|
20
|
+
* False on legacy Windows cmd.exe — callers should fall back to ASCII symbols.
|
|
21
|
+
*
|
|
22
|
+
* Generated via String.fromCodePoint() (never literal Unicode in source)
|
|
23
|
+
* to prevent UTF-8 → Latin-1 corruption when the npm package is bundled.
|
|
24
|
+
*/
|
|
25
|
+
export const isUnicodeSupported: boolean =
|
|
26
|
+
process.platform !== 'win32' ||
|
|
27
|
+
Boolean(process.env.WT_SESSION) ||
|
|
28
|
+
process.env.TERM_PROGRAM === 'vscode';
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { getConfigDir } from './config';
|
|
4
|
+
import { VERSION } from './version';
|
|
5
|
+
|
|
6
|
+
const CHECK_INTERVAL_MS = 1 * 60 * 60 * 1000; // 1 hour
|
|
7
|
+
export const GITHUB_RELEASES_URL =
|
|
8
|
+
'https://api.github.com/repos/resend/resend-cli/releases/latest';
|
|
9
|
+
|
|
10
|
+
type UpdateState = {
|
|
11
|
+
lastChecked: number;
|
|
12
|
+
latestVersion: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function getStatePath(): string {
|
|
16
|
+
return join(getConfigDir(), 'update-state.json');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function readState(): UpdateState | null {
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(readFileSync(getStatePath(), 'utf-8')) as UpdateState;
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function writeState(state: UpdateState): void {
|
|
28
|
+
mkdirSync(getConfigDir(), { recursive: true, mode: 0o700 });
|
|
29
|
+
writeFileSync(getStatePath(), JSON.stringify(state), { mode: 0o600 });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Compare two semver strings. Returns true if remote > local.
|
|
34
|
+
*/
|
|
35
|
+
function isNewer(local: string, remote: string): boolean {
|
|
36
|
+
const parse = (v: string) => v.replace(/^v/, '').split('.').map(Number);
|
|
37
|
+
const [lMaj, lMin, lPat] = parse(local);
|
|
38
|
+
const [rMaj, rMin, rPat] = parse(remote);
|
|
39
|
+
if (rMaj !== lMaj) {
|
|
40
|
+
return rMaj > lMaj;
|
|
41
|
+
}
|
|
42
|
+
if (rMin !== lMin) {
|
|
43
|
+
return rMin > lMin;
|
|
44
|
+
}
|
|
45
|
+
return rPat > lPat;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function fetchLatestVersion(): Promise<string | null> {
|
|
49
|
+
try {
|
|
50
|
+
const res = await fetch(GITHUB_RELEASES_URL, {
|
|
51
|
+
headers: { Accept: 'application/vnd.github.v3+json' },
|
|
52
|
+
signal: AbortSignal.timeout(5000),
|
|
53
|
+
});
|
|
54
|
+
if (!res.ok) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const data = (await res.json()) as {
|
|
58
|
+
tag_name?: string;
|
|
59
|
+
prerelease?: boolean;
|
|
60
|
+
draft?: boolean;
|
|
61
|
+
};
|
|
62
|
+
// /releases/latest already excludes prereleases, but guard anyway
|
|
63
|
+
if (data.prerelease || data.draft) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const version = data.tag_name?.replace(/^v/, '');
|
|
67
|
+
if (!version || !/^\d+\.\d+\.\d+$/.test(version)) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
return version;
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function shouldSkipCheck(): boolean {
|
|
77
|
+
if (process.env.RESEND_NO_UPDATE_NOTIFIER === '1') {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
if (process.env.CI === 'true' || process.env.CI === '1') {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
if (process.env.GITHUB_ACTIONS) {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
if (!process.stdout.isTTY) {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function detectInstallMethod(): string {
|
|
93
|
+
const execPath = process.execPath || process.argv[0] || '';
|
|
94
|
+
|
|
95
|
+
// Homebrew
|
|
96
|
+
if (/\/(Cellar|homebrew)\//i.test(execPath)) {
|
|
97
|
+
return 'brew upgrade resend';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// npm / npx global install
|
|
101
|
+
if (/node_modules/.test(execPath) || process.env.npm_execpath) {
|
|
102
|
+
return 'npm install -g resend-cli';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Install script (default install location)
|
|
106
|
+
if (/[/\\]\.resend[/\\]bin[/\\]/.test(execPath)) {
|
|
107
|
+
if (process.platform === 'win32') {
|
|
108
|
+
return 'irm https://resend.com/install.ps1 | iex';
|
|
109
|
+
}
|
|
110
|
+
return 'curl -fsSL https://resend.com/install.sh | bash';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Default: install script
|
|
114
|
+
if (process.platform === 'win32') {
|
|
115
|
+
return 'irm https://resend.com/install.ps1 | iex';
|
|
116
|
+
}
|
|
117
|
+
return 'curl -fsSL https://resend.com/install.sh | bash';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function formatNotice(latestVersion: string): string {
|
|
121
|
+
const upgrade = detectInstallMethod();
|
|
122
|
+
const isUrl = upgrade.startsWith('http');
|
|
123
|
+
|
|
124
|
+
const dim = '\x1B[2m';
|
|
125
|
+
const yellow = '\x1B[33m';
|
|
126
|
+
const cyan = '\x1B[36m';
|
|
127
|
+
const reset = '\x1B[0m';
|
|
128
|
+
|
|
129
|
+
return [
|
|
130
|
+
'',
|
|
131
|
+
`${dim}Update available: ${yellow}v${VERSION}${reset}${dim} → ${cyan}v${latestVersion}${reset}`,
|
|
132
|
+
`${dim}${isUrl ? 'Visit' : 'Run'}: ${cyan}${upgrade}${reset}`,
|
|
133
|
+
'',
|
|
134
|
+
].join('\n');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Check for updates and print a notice to stderr if one is available.
|
|
139
|
+
* Designed to be called after the main command completes — never blocks
|
|
140
|
+
* or throws.
|
|
141
|
+
*/
|
|
142
|
+
export async function checkForUpdates(): Promise<void> {
|
|
143
|
+
if (shouldSkipCheck()) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const state = readState();
|
|
148
|
+
const now = Date.now();
|
|
149
|
+
|
|
150
|
+
// If we have a cached check that's still fresh, just use it
|
|
151
|
+
if (state && now - state.lastChecked < CHECK_INTERVAL_MS) {
|
|
152
|
+
if (isNewer(VERSION, state.latestVersion)) {
|
|
153
|
+
process.stderr.write(formatNotice(state.latestVersion));
|
|
154
|
+
}
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Stale or missing — fetch in the background
|
|
159
|
+
const latest = await fetchLatestVersion();
|
|
160
|
+
if (!latest) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
writeState({ lastChecked: now, latestVersion: latest });
|
|
165
|
+
|
|
166
|
+
if (isNewer(VERSION, latest)) {
|
|
167
|
+
process.stderr.write(formatNotice(latest));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import {
|
|
2
|
+
afterEach,
|
|
3
|
+
beforeEach,
|
|
4
|
+
describe,
|
|
5
|
+
expect,
|
|
6
|
+
type MockInstance,
|
|
7
|
+
test,
|
|
8
|
+
vi,
|
|
9
|
+
} from 'vitest';
|
|
10
|
+
import {
|
|
11
|
+
captureTestEnv,
|
|
12
|
+
expectExit1,
|
|
13
|
+
mockExitThrow,
|
|
14
|
+
mockSdkError,
|
|
15
|
+
setNonInteractive,
|
|
16
|
+
setupOutputSpies,
|
|
17
|
+
} from '../../helpers';
|
|
18
|
+
|
|
19
|
+
const mockCreate = vi.fn(async () => ({
|
|
20
|
+
data: { id: 'test-key-id', token: 're_testtoken1234567890' },
|
|
21
|
+
error: null,
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
vi.mock('resend', () => ({
|
|
25
|
+
Resend: class MockResend {
|
|
26
|
+
constructor(public key: string) {}
|
|
27
|
+
apiKeys = { create: mockCreate };
|
|
28
|
+
},
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
describe('api-keys create command', () => {
|
|
32
|
+
const restoreEnv = captureTestEnv();
|
|
33
|
+
let spies: ReturnType<typeof setupOutputSpies> | undefined;
|
|
34
|
+
let errorSpy: MockInstance | undefined;
|
|
35
|
+
let stderrSpy: MockInstance | undefined;
|
|
36
|
+
let exitSpy: MockInstance | undefined;
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
process.env.RESEND_API_KEY = 're_test_key';
|
|
40
|
+
mockCreate.mockClear();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
restoreEnv();
|
|
45
|
+
errorSpy?.mockRestore();
|
|
46
|
+
stderrSpy?.mockRestore();
|
|
47
|
+
exitSpy?.mockRestore();
|
|
48
|
+
spies = undefined;
|
|
49
|
+
errorSpy = undefined;
|
|
50
|
+
stderrSpy = undefined;
|
|
51
|
+
exitSpy = undefined;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('creates API key with --name flag', async () => {
|
|
55
|
+
spies = setupOutputSpies();
|
|
56
|
+
|
|
57
|
+
const { createApiKeyCommand } = await import(
|
|
58
|
+
'../../../src/commands/api-keys/create'
|
|
59
|
+
);
|
|
60
|
+
await createApiKeyCommand.parseAsync(['--name', 'Production'], {
|
|
61
|
+
from: 'user',
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
expect(mockCreate).toHaveBeenCalledTimes(1);
|
|
65
|
+
const args = mockCreate.mock.calls[0][0] as Record<string, unknown>;
|
|
66
|
+
expect(args.name).toBe('Production');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('passes permission flag to SDK', async () => {
|
|
70
|
+
spies = setupOutputSpies();
|
|
71
|
+
|
|
72
|
+
const { createApiKeyCommand } = await import(
|
|
73
|
+
'../../../src/commands/api-keys/create'
|
|
74
|
+
);
|
|
75
|
+
await createApiKeyCommand.parseAsync(
|
|
76
|
+
['--name', 'CI Token', '--permission', 'sending_access'],
|
|
77
|
+
{ from: 'user' },
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const args = mockCreate.mock.calls[0][0] as Record<string, unknown>;
|
|
81
|
+
expect(args.permission).toBe('sending_access');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('passes domain_id (snake_case) to SDK when --domain-id is provided', async () => {
|
|
85
|
+
spies = setupOutputSpies();
|
|
86
|
+
|
|
87
|
+
const { createApiKeyCommand } = await import(
|
|
88
|
+
'../../../src/commands/api-keys/create'
|
|
89
|
+
);
|
|
90
|
+
await createApiKeyCommand.parseAsync(
|
|
91
|
+
[
|
|
92
|
+
'--name',
|
|
93
|
+
'Domain Token',
|
|
94
|
+
'--permission',
|
|
95
|
+
'sending_access',
|
|
96
|
+
'--domain-id',
|
|
97
|
+
'domain-123',
|
|
98
|
+
],
|
|
99
|
+
{ from: 'user' },
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const args = mockCreate.mock.calls[0][0] as Record<string, unknown>;
|
|
103
|
+
expect(args.domain_id).toBe('domain-123');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('outputs JSON result when non-interactive', async () => {
|
|
107
|
+
spies = setupOutputSpies();
|
|
108
|
+
|
|
109
|
+
const { createApiKeyCommand } = await import(
|
|
110
|
+
'../../../src/commands/api-keys/create'
|
|
111
|
+
);
|
|
112
|
+
await createApiKeyCommand.parseAsync(['--name', 'Production'], {
|
|
113
|
+
from: 'user',
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const output = spies.logSpy.mock.calls[0][0] as string;
|
|
117
|
+
const parsed = JSON.parse(output);
|
|
118
|
+
expect(parsed.id).toBe('test-key-id');
|
|
119
|
+
expect(parsed.token).toBe('re_testtoken1234567890');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('errors with missing_name when --name absent in non-interactive mode', async () => {
|
|
123
|
+
setNonInteractive();
|
|
124
|
+
errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
125
|
+
exitSpy = mockExitThrow();
|
|
126
|
+
|
|
127
|
+
const { createApiKeyCommand } = await import(
|
|
128
|
+
'../../../src/commands/api-keys/create'
|
|
129
|
+
);
|
|
130
|
+
await expectExit1(() =>
|
|
131
|
+
createApiKeyCommand.parseAsync([], { from: 'user' }),
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const output = errorSpy.mock.calls.map((c) => c[0]).join(' ');
|
|
135
|
+
expect(output).toContain('missing_name');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('does not call SDK when missing_name error is raised', async () => {
|
|
139
|
+
setNonInteractive();
|
|
140
|
+
errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
141
|
+
exitSpy = mockExitThrow();
|
|
142
|
+
|
|
143
|
+
const { createApiKeyCommand } = await import(
|
|
144
|
+
'../../../src/commands/api-keys/create'
|
|
145
|
+
);
|
|
146
|
+
await expectExit1(() =>
|
|
147
|
+
createApiKeyCommand.parseAsync([], { from: 'user' }),
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
expect(mockCreate).not.toHaveBeenCalled();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('errors with auth_error when no API key', async () => {
|
|
154
|
+
setNonInteractive();
|
|
155
|
+
delete process.env.RESEND_API_KEY;
|
|
156
|
+
process.env.XDG_CONFIG_HOME = '/tmp/nonexistent-resend';
|
|
157
|
+
errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
158
|
+
exitSpy = mockExitThrow();
|
|
159
|
+
|
|
160
|
+
const { createApiKeyCommand } = await import(
|
|
161
|
+
'../../../src/commands/api-keys/create'
|
|
162
|
+
);
|
|
163
|
+
await expectExit1(() =>
|
|
164
|
+
createApiKeyCommand.parseAsync(['--name', 'Production'], {
|
|
165
|
+
from: 'user',
|
|
166
|
+
}),
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const output = errorSpy.mock.calls.map((c) => c[0]).join(' ');
|
|
170
|
+
expect(output).toContain('auth_error');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('errors with create_error when SDK returns an error', async () => {
|
|
174
|
+
setNonInteractive();
|
|
175
|
+
mockCreate.mockResolvedValueOnce(
|
|
176
|
+
mockSdkError('Name already taken', 'validation_error'),
|
|
177
|
+
);
|
|
178
|
+
errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
179
|
+
stderrSpy = vi
|
|
180
|
+
.spyOn(process.stderr, 'write')
|
|
181
|
+
.mockImplementation(() => true);
|
|
182
|
+
exitSpy = mockExitThrow();
|
|
183
|
+
|
|
184
|
+
const { createApiKeyCommand } = await import(
|
|
185
|
+
'../../../src/commands/api-keys/create'
|
|
186
|
+
);
|
|
187
|
+
await expectExit1(() =>
|
|
188
|
+
createApiKeyCommand.parseAsync(['--name', 'Production'], {
|
|
189
|
+
from: 'user',
|
|
190
|
+
}),
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const output = errorSpy.mock.calls.map((c) => c[0]).join(' ');
|
|
194
|
+
expect(output).toContain('create_error');
|
|
195
|
+
});
|
|
196
|
+
});
|