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.
Files changed (191) hide show
  1. package/__tests__/resend-plugin.test.js +22 -1
  2. package/package.json +1 -1
  3. package/plugins/plugins.json +2 -2
  4. package/plugins/resend/plugin.json +279 -2
  5. package/plugins/resend/skills/quickstart/SKILL.md +32 -13
  6. package/temp_resend_cli/repo/.github/scripts/pr-title-check.js +34 -0
  7. package/temp_resend_cli/repo/.github/workflows/ci.yml +67 -0
  8. package/temp_resend_cli/repo/.github/workflows/post-release.yml +51 -0
  9. package/temp_resend_cli/repo/.github/workflows/pr-title-check.yml +13 -0
  10. package/temp_resend_cli/repo/.github/workflows/release.yml +175 -0
  11. package/temp_resend_cli/repo/.github/workflows/test-install-unix.yml +34 -0
  12. package/temp_resend_cli/repo/.github/workflows/test-install-windows.yml +48 -0
  13. package/temp_resend_cli/repo/CHANGELOG.md +31 -0
  14. package/temp_resend_cli/repo/LICENSE +21 -0
  15. package/temp_resend_cli/repo/README.md +450 -0
  16. package/temp_resend_cli/repo/biome.json +36 -0
  17. package/temp_resend_cli/repo/install.ps1 +141 -0
  18. package/temp_resend_cli/repo/install.sh +301 -0
  19. package/temp_resend_cli/repo/package.json +61 -0
  20. package/temp_resend_cli/repo/pnpm-lock.yaml +2439 -0
  21. package/temp_resend_cli/repo/renovate.json +4 -0
  22. package/temp_resend_cli/repo/src/cli.ts +98 -0
  23. package/temp_resend_cli/repo/src/commands/api-keys/create.ts +114 -0
  24. package/temp_resend_cli/repo/src/commands/api-keys/delete.ts +47 -0
  25. package/temp_resend_cli/repo/src/commands/api-keys/index.ts +26 -0
  26. package/temp_resend_cli/repo/src/commands/api-keys/list.ts +35 -0
  27. package/temp_resend_cli/repo/src/commands/api-keys/utils.ts +8 -0
  28. package/temp_resend_cli/repo/src/commands/auth/index.ts +20 -0
  29. package/temp_resend_cli/repo/src/commands/auth/login.ts +234 -0
  30. package/temp_resend_cli/repo/src/commands/auth/logout.ts +105 -0
  31. package/temp_resend_cli/repo/src/commands/broadcasts/create.ts +196 -0
  32. package/temp_resend_cli/repo/src/commands/broadcasts/delete.ts +46 -0
  33. package/temp_resend_cli/repo/src/commands/broadcasts/get.ts +59 -0
  34. package/temp_resend_cli/repo/src/commands/broadcasts/index.ts +43 -0
  35. package/temp_resend_cli/repo/src/commands/broadcasts/list.ts +60 -0
  36. package/temp_resend_cli/repo/src/commands/broadcasts/send.ts +56 -0
  37. package/temp_resend_cli/repo/src/commands/broadcasts/update.ts +95 -0
  38. package/temp_resend_cli/repo/src/commands/broadcasts/utils.ts +35 -0
  39. package/temp_resend_cli/repo/src/commands/contact-properties/create.ts +118 -0
  40. package/temp_resend_cli/repo/src/commands/contact-properties/delete.ts +48 -0
  41. package/temp_resend_cli/repo/src/commands/contact-properties/get.ts +46 -0
  42. package/temp_resend_cli/repo/src/commands/contact-properties/index.ts +48 -0
  43. package/temp_resend_cli/repo/src/commands/contact-properties/list.ts +68 -0
  44. package/temp_resend_cli/repo/src/commands/contact-properties/update.ts +88 -0
  45. package/temp_resend_cli/repo/src/commands/contact-properties/utils.ts +17 -0
  46. package/temp_resend_cli/repo/src/commands/contacts/add-segment.ts +78 -0
  47. package/temp_resend_cli/repo/src/commands/contacts/create.ts +122 -0
  48. package/temp_resend_cli/repo/src/commands/contacts/delete.ts +49 -0
  49. package/temp_resend_cli/repo/src/commands/contacts/get.ts +53 -0
  50. package/temp_resend_cli/repo/src/commands/contacts/index.ts +58 -0
  51. package/temp_resend_cli/repo/src/commands/contacts/list.ts +57 -0
  52. package/temp_resend_cli/repo/src/commands/contacts/remove-segment.ts +48 -0
  53. package/temp_resend_cli/repo/src/commands/contacts/segments.ts +39 -0
  54. package/temp_resend_cli/repo/src/commands/contacts/topics.ts +45 -0
  55. package/temp_resend_cli/repo/src/commands/contacts/update-topics.ts +90 -0
  56. package/temp_resend_cli/repo/src/commands/contacts/update.ts +77 -0
  57. package/temp_resend_cli/repo/src/commands/contacts/utils.ts +119 -0
  58. package/temp_resend_cli/repo/src/commands/doctor.ts +216 -0
  59. package/temp_resend_cli/repo/src/commands/domains/create.ts +83 -0
  60. package/temp_resend_cli/repo/src/commands/domains/delete.ts +42 -0
  61. package/temp_resend_cli/repo/src/commands/domains/get.ts +47 -0
  62. package/temp_resend_cli/repo/src/commands/domains/index.ts +35 -0
  63. package/temp_resend_cli/repo/src/commands/domains/list.ts +53 -0
  64. package/temp_resend_cli/repo/src/commands/domains/update.ts +75 -0
  65. package/temp_resend_cli/repo/src/commands/domains/utils.ts +44 -0
  66. package/temp_resend_cli/repo/src/commands/domains/verify.ts +38 -0
  67. package/temp_resend_cli/repo/src/commands/emails/batch.ts +140 -0
  68. package/temp_resend_cli/repo/src/commands/emails/get.ts +44 -0
  69. package/temp_resend_cli/repo/src/commands/emails/index.ts +30 -0
  70. package/temp_resend_cli/repo/src/commands/emails/list.ts +84 -0
  71. package/temp_resend_cli/repo/src/commands/emails/receiving/attachment.ts +55 -0
  72. package/temp_resend_cli/repo/src/commands/emails/receiving/attachments.ts +68 -0
  73. package/temp_resend_cli/repo/src/commands/emails/receiving/get.ts +58 -0
  74. package/temp_resend_cli/repo/src/commands/emails/receiving/index.ts +28 -0
  75. package/temp_resend_cli/repo/src/commands/emails/receiving/list.ts +59 -0
  76. package/temp_resend_cli/repo/src/commands/emails/receiving/utils.ts +38 -0
  77. package/temp_resend_cli/repo/src/commands/emails/send.ts +189 -0
  78. package/temp_resend_cli/repo/src/commands/open.ts +27 -0
  79. package/temp_resend_cli/repo/src/commands/segments/create.ts +50 -0
  80. package/temp_resend_cli/repo/src/commands/segments/delete.ts +47 -0
  81. package/temp_resend_cli/repo/src/commands/segments/get.ts +38 -0
  82. package/temp_resend_cli/repo/src/commands/segments/index.ts +36 -0
  83. package/temp_resend_cli/repo/src/commands/segments/list.ts +58 -0
  84. package/temp_resend_cli/repo/src/commands/segments/utils.ts +7 -0
  85. package/temp_resend_cli/repo/src/commands/teams/index.ts +10 -0
  86. package/temp_resend_cli/repo/src/commands/teams/list.ts +35 -0
  87. package/temp_resend_cli/repo/src/commands/teams/remove.ts +86 -0
  88. package/temp_resend_cli/repo/src/commands/teams/switch.ts +76 -0
  89. package/temp_resend_cli/repo/src/commands/topics/create.ts +73 -0
  90. package/temp_resend_cli/repo/src/commands/topics/delete.ts +47 -0
  91. package/temp_resend_cli/repo/src/commands/topics/get.ts +42 -0
  92. package/temp_resend_cli/repo/src/commands/topics/index.ts +42 -0
  93. package/temp_resend_cli/repo/src/commands/topics/list.ts +34 -0
  94. package/temp_resend_cli/repo/src/commands/topics/update.ts +59 -0
  95. package/temp_resend_cli/repo/src/commands/topics/utils.ts +16 -0
  96. package/temp_resend_cli/repo/src/commands/webhooks/create.ts +128 -0
  97. package/temp_resend_cli/repo/src/commands/webhooks/delete.ts +49 -0
  98. package/temp_resend_cli/repo/src/commands/webhooks/get.ts +42 -0
  99. package/temp_resend_cli/repo/src/commands/webhooks/index.ts +42 -0
  100. package/temp_resend_cli/repo/src/commands/webhooks/list.ts +55 -0
  101. package/temp_resend_cli/repo/src/commands/webhooks/listen.ts +379 -0
  102. package/temp_resend_cli/repo/src/commands/webhooks/update.ts +83 -0
  103. package/temp_resend_cli/repo/src/commands/webhooks/utils.ts +36 -0
  104. package/temp_resend_cli/repo/src/commands/whoami.ts +71 -0
  105. package/temp_resend_cli/repo/src/lib/actions.ts +157 -0
  106. package/temp_resend_cli/repo/src/lib/client.ts +37 -0
  107. package/temp_resend_cli/repo/src/lib/config.ts +217 -0
  108. package/temp_resend_cli/repo/src/lib/files.ts +15 -0
  109. package/temp_resend_cli/repo/src/lib/help-text.ts +38 -0
  110. package/temp_resend_cli/repo/src/lib/output.ts +56 -0
  111. package/temp_resend_cli/repo/src/lib/pagination.ts +36 -0
  112. package/temp_resend_cli/repo/src/lib/prompts.ts +149 -0
  113. package/temp_resend_cli/repo/src/lib/spinner.ts +100 -0
  114. package/temp_resend_cli/repo/src/lib/table.ts +57 -0
  115. package/temp_resend_cli/repo/src/lib/tty.ts +28 -0
  116. package/temp_resend_cli/repo/src/lib/update-check.ts +169 -0
  117. package/temp_resend_cli/repo/src/lib/version.ts +4 -0
  118. package/temp_resend_cli/repo/tests/commands/api-keys/create.test.ts +196 -0
  119. package/temp_resend_cli/repo/tests/commands/api-keys/delete.test.ts +157 -0
  120. package/temp_resend_cli/repo/tests/commands/api-keys/list.test.ts +134 -0
  121. package/temp_resend_cli/repo/tests/commands/auth/login.test.ts +153 -0
  122. package/temp_resend_cli/repo/tests/commands/auth/logout.test.ts +153 -0
  123. package/temp_resend_cli/repo/tests/commands/broadcasts/create.test.ts +454 -0
  124. package/temp_resend_cli/repo/tests/commands/broadcasts/delete.test.ts +183 -0
  125. package/temp_resend_cli/repo/tests/commands/broadcasts/get.test.ts +147 -0
  126. package/temp_resend_cli/repo/tests/commands/broadcasts/list.test.ts +199 -0
  127. package/temp_resend_cli/repo/tests/commands/broadcasts/send.test.ts +162 -0
  128. package/temp_resend_cli/repo/tests/commands/broadcasts/update.test.ts +288 -0
  129. package/temp_resend_cli/repo/tests/commands/contact-properties/create.test.ts +251 -0
  130. package/temp_resend_cli/repo/tests/commands/contact-properties/delete.test.ts +184 -0
  131. package/temp_resend_cli/repo/tests/commands/contact-properties/get.test.ts +145 -0
  132. package/temp_resend_cli/repo/tests/commands/contact-properties/list.test.ts +181 -0
  133. package/temp_resend_cli/repo/tests/commands/contact-properties/update.test.ts +217 -0
  134. package/temp_resend_cli/repo/tests/commands/contacts/add-segment.test.ts +189 -0
  135. package/temp_resend_cli/repo/tests/commands/contacts/create.test.ts +271 -0
  136. package/temp_resend_cli/repo/tests/commands/contacts/delete.test.ts +193 -0
  137. package/temp_resend_cli/repo/tests/commands/contacts/get.test.ts +149 -0
  138. package/temp_resend_cli/repo/tests/commands/contacts/list.test.ts +176 -0
  139. package/temp_resend_cli/repo/tests/commands/contacts/remove-segment.test.ts +167 -0
  140. package/temp_resend_cli/repo/tests/commands/contacts/segments.test.ts +168 -0
  141. package/temp_resend_cli/repo/tests/commands/contacts/topics.test.ts +164 -0
  142. package/temp_resend_cli/repo/tests/commands/contacts/update-topics.test.ts +248 -0
  143. package/temp_resend_cli/repo/tests/commands/contacts/update.test.ts +206 -0
  144. package/temp_resend_cli/repo/tests/commands/doctor.test.ts +164 -0
  145. package/temp_resend_cli/repo/tests/commands/domains/create.test.ts +193 -0
  146. package/temp_resend_cli/repo/tests/commands/domains/delete.test.ts +157 -0
  147. package/temp_resend_cli/repo/tests/commands/domains/get.test.ts +138 -0
  148. package/temp_resend_cli/repo/tests/commands/domains/list.test.ts +165 -0
  149. package/temp_resend_cli/repo/tests/commands/domains/update.test.ts +224 -0
  150. package/temp_resend_cli/repo/tests/commands/domains/verify.test.ts +118 -0
  151. package/temp_resend_cli/repo/tests/commands/emails/batch.test.ts +324 -0
  152. package/temp_resend_cli/repo/tests/commands/emails/get.test.ts +132 -0
  153. package/temp_resend_cli/repo/tests/commands/emails/receiving/attachment.test.ts +141 -0
  154. package/temp_resend_cli/repo/tests/commands/emails/receiving/attachments.test.ts +169 -0
  155. package/temp_resend_cli/repo/tests/commands/emails/receiving/get.test.ts +141 -0
  156. package/temp_resend_cli/repo/tests/commands/emails/receiving/list.test.ts +182 -0
  157. package/temp_resend_cli/repo/tests/commands/emails/send.test.ts +312 -0
  158. package/temp_resend_cli/repo/tests/commands/segments/create.test.ts +164 -0
  159. package/temp_resend_cli/repo/tests/commands/segments/delete.test.ts +183 -0
  160. package/temp_resend_cli/repo/tests/commands/segments/get.test.ts +138 -0
  161. package/temp_resend_cli/repo/tests/commands/segments/list.test.ts +174 -0
  162. package/temp_resend_cli/repo/tests/commands/teams/list.test.ts +62 -0
  163. package/temp_resend_cli/repo/tests/commands/teams/remove.test.ts +110 -0
  164. package/temp_resend_cli/repo/tests/commands/teams/switch.test.ts +103 -0
  165. package/temp_resend_cli/repo/tests/commands/topics/create.test.ts +192 -0
  166. package/temp_resend_cli/repo/tests/commands/topics/delete.test.ts +157 -0
  167. package/temp_resend_cli/repo/tests/commands/topics/get.test.ts +126 -0
  168. package/temp_resend_cli/repo/tests/commands/topics/list.test.ts +125 -0
  169. package/temp_resend_cli/repo/tests/commands/topics/update.test.ts +178 -0
  170. package/temp_resend_cli/repo/tests/commands/webhooks/create.test.ts +225 -0
  171. package/temp_resend_cli/repo/tests/commands/webhooks/delete.test.ts +157 -0
  172. package/temp_resend_cli/repo/tests/commands/webhooks/get.test.ts +126 -0
  173. package/temp_resend_cli/repo/tests/commands/webhooks/list.test.ts +178 -0
  174. package/temp_resend_cli/repo/tests/commands/webhooks/update.test.ts +207 -0
  175. package/temp_resend_cli/repo/tests/commands/whoami.test.ts +98 -0
  176. package/temp_resend_cli/repo/tests/e2e/smoke.test.ts +93 -0
  177. package/temp_resend_cli/repo/tests/helpers.ts +86 -0
  178. package/temp_resend_cli/repo/tests/lib/client.test.ts +71 -0
  179. package/temp_resend_cli/repo/tests/lib/config.test.ts +451 -0
  180. package/temp_resend_cli/repo/tests/lib/files.test.ts +73 -0
  181. package/temp_resend_cli/repo/tests/lib/help-text.test.ts +97 -0
  182. package/temp_resend_cli/repo/tests/lib/output.test.ts +136 -0
  183. package/temp_resend_cli/repo/tests/lib/prompts.test.ts +185 -0
  184. package/temp_resend_cli/repo/tests/lib/spinner.test.ts +166 -0
  185. package/temp_resend_cli/repo/tests/lib/table.test.ts +63 -0
  186. package/temp_resend_cli/repo/tests/lib/tty.test.ts +89 -0
  187. package/temp_resend_cli/repo/tests/lib/update-check.test.ts +179 -0
  188. package/temp_resend_cli/repo/tsconfig.json +14 -0
  189. package/temp_resend_cli/repo/vitest.config.e2e.ts +8 -0
  190. package/temp_resend_cli/repo/vitest.config.ts +10 -0
  191. package/tests/test-resend-smoke.sh +7 -3
@@ -0,0 +1,157 @@
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 mockRemove = vi.fn(async () => ({
20
+ data: {},
21
+ error: null,
22
+ }));
23
+
24
+ vi.mock('resend', () => ({
25
+ Resend: class MockResend {
26
+ constructor(public key: string) {}
27
+ apiKeys = { remove: mockRemove };
28
+ },
29
+ }));
30
+
31
+ describe('api-keys delete 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
+ mockRemove.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('deletes API key with --yes flag', async () => {
55
+ spies = setupOutputSpies();
56
+
57
+ const { deleteApiKeyCommand } = await import(
58
+ '../../../src/commands/api-keys/delete'
59
+ );
60
+ await deleteApiKeyCommand.parseAsync(['test-key-id', '--yes'], {
61
+ from: 'user',
62
+ });
63
+
64
+ expect(mockRemove).toHaveBeenCalledWith('test-key-id');
65
+ });
66
+
67
+ test('outputs synthesized deleted JSON on success', async () => {
68
+ spies = setupOutputSpies();
69
+
70
+ const { deleteApiKeyCommand } = await import(
71
+ '../../../src/commands/api-keys/delete'
72
+ );
73
+ await deleteApiKeyCommand.parseAsync(['test-key-id', '--yes'], {
74
+ from: 'user',
75
+ });
76
+
77
+ const output = spies.logSpy.mock.calls[0][0] as string;
78
+ const parsed = JSON.parse(output);
79
+ expect(parsed.deleted).toBe(true);
80
+ expect(parsed.id).toBe('test-key-id');
81
+ });
82
+
83
+ test('errors with confirmation_required when --yes absent in non-interactive mode', async () => {
84
+ setNonInteractive();
85
+ errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
86
+ exitSpy = mockExitThrow();
87
+
88
+ const { deleteApiKeyCommand } = await import(
89
+ '../../../src/commands/api-keys/delete'
90
+ );
91
+ await expectExit1(() =>
92
+ deleteApiKeyCommand.parseAsync(['test-key-id'], { from: 'user' }),
93
+ );
94
+
95
+ const output = errorSpy.mock.calls.map((c) => c[0]).join(' ');
96
+ expect(output).toContain('confirmation_required');
97
+ });
98
+
99
+ test('does not call SDK when confirmation is required but not given', async () => {
100
+ setNonInteractive();
101
+ errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
102
+ exitSpy = mockExitThrow();
103
+
104
+ const { deleteApiKeyCommand } = await import(
105
+ '../../../src/commands/api-keys/delete'
106
+ );
107
+ await expectExit1(() =>
108
+ deleteApiKeyCommand.parseAsync(['test-key-id'], { from: 'user' }),
109
+ );
110
+
111
+ expect(mockRemove).not.toHaveBeenCalled();
112
+ });
113
+
114
+ test('errors with auth_error when no API key', async () => {
115
+ setNonInteractive();
116
+ delete process.env.RESEND_API_KEY;
117
+ process.env.XDG_CONFIG_HOME = '/tmp/nonexistent-resend';
118
+ errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
119
+ exitSpy = mockExitThrow();
120
+
121
+ const { deleteApiKeyCommand } = await import(
122
+ '../../../src/commands/api-keys/delete'
123
+ );
124
+ await expectExit1(() =>
125
+ deleteApiKeyCommand.parseAsync(['test-key-id', '--yes'], {
126
+ from: 'user',
127
+ }),
128
+ );
129
+
130
+ const output = errorSpy.mock.calls.map((c) => c[0]).join(' ');
131
+ expect(output).toContain('auth_error');
132
+ });
133
+
134
+ test('errors with delete_error when SDK returns an error', async () => {
135
+ setNonInteractive();
136
+ mockRemove.mockResolvedValueOnce(
137
+ mockSdkError('API key not found', 'not_found'),
138
+ );
139
+ errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
140
+ stderrSpy = vi
141
+ .spyOn(process.stderr, 'write')
142
+ .mockImplementation(() => true);
143
+ exitSpy = mockExitThrow();
144
+
145
+ const { deleteApiKeyCommand } = await import(
146
+ '../../../src/commands/api-keys/delete'
147
+ );
148
+ await expectExit1(() =>
149
+ deleteApiKeyCommand.parseAsync(['test-key-id', '--yes'], {
150
+ from: 'user',
151
+ }),
152
+ );
153
+
154
+ const output = errorSpy.mock.calls.map((c) => c[0]).join(' ');
155
+ expect(output).toContain('delete_error');
156
+ });
157
+ });
@@ -0,0 +1,134 @@
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 mockList = vi.fn(async () => ({
20
+ data: {
21
+ object: 'list',
22
+ data: [
23
+ {
24
+ id: 'key-id-1',
25
+ name: 'Production Key',
26
+ created_at: '2026-01-01T00:00:00.000Z',
27
+ },
28
+ {
29
+ id: 'key-id-2',
30
+ name: 'Staging Key',
31
+ created_at: '2026-01-02T00:00:00.000Z',
32
+ },
33
+ ],
34
+ },
35
+ error: null,
36
+ }));
37
+
38
+ vi.mock('resend', () => ({
39
+ Resend: class MockResend {
40
+ constructor(public key: string) {}
41
+ apiKeys = { list: mockList };
42
+ },
43
+ }));
44
+
45
+ describe('api-keys list command', () => {
46
+ const restoreEnv = captureTestEnv();
47
+ let spies: ReturnType<typeof setupOutputSpies> | undefined;
48
+ let errorSpy: MockInstance | undefined;
49
+ let stderrSpy: MockInstance | undefined;
50
+ let exitSpy: MockInstance | undefined;
51
+
52
+ beforeEach(() => {
53
+ process.env.RESEND_API_KEY = 're_test_key';
54
+ mockList.mockClear();
55
+ });
56
+
57
+ afterEach(() => {
58
+ restoreEnv();
59
+ errorSpy?.mockRestore();
60
+ stderrSpy?.mockRestore();
61
+ exitSpy?.mockRestore();
62
+ spies = undefined;
63
+ errorSpy = undefined;
64
+ stderrSpy = undefined;
65
+ exitSpy = undefined;
66
+ });
67
+
68
+ test('calls SDK list with no arguments', async () => {
69
+ spies = setupOutputSpies();
70
+
71
+ const { listApiKeysCommand } = await import(
72
+ '../../../src/commands/api-keys/list'
73
+ );
74
+ await listApiKeysCommand.parseAsync([], { from: 'user' });
75
+
76
+ expect(mockList).toHaveBeenCalledTimes(1);
77
+ });
78
+
79
+ test('outputs JSON list when non-interactive', async () => {
80
+ spies = setupOutputSpies();
81
+
82
+ const { listApiKeysCommand } = await import(
83
+ '../../../src/commands/api-keys/list'
84
+ );
85
+ await listApiKeysCommand.parseAsync([], { from: 'user' });
86
+
87
+ const output = spies.logSpy.mock.calls[0][0] as string;
88
+ const parsed = JSON.parse(output);
89
+ expect(parsed.object).toBe('list');
90
+ expect(parsed.data).toHaveLength(2);
91
+ expect(parsed.data[0].id).toBe('key-id-1');
92
+ expect(parsed.data[0].name).toBe('Production Key');
93
+ });
94
+
95
+ test('errors with auth_error when no API key', async () => {
96
+ setNonInteractive();
97
+ delete process.env.RESEND_API_KEY;
98
+ process.env.XDG_CONFIG_HOME = '/tmp/nonexistent-resend';
99
+ errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
100
+ exitSpy = mockExitThrow();
101
+
102
+ const { listApiKeysCommand } = await import(
103
+ '../../../src/commands/api-keys/list'
104
+ );
105
+ await expectExit1(() =>
106
+ listApiKeysCommand.parseAsync([], { from: 'user' }),
107
+ );
108
+
109
+ const output = errorSpy.mock.calls.map((c) => c[0]).join(' ');
110
+ expect(output).toContain('auth_error');
111
+ });
112
+
113
+ test('errors with list_error when SDK returns an error', async () => {
114
+ setNonInteractive();
115
+ mockList.mockResolvedValueOnce(
116
+ mockSdkError('Unauthorized', 'unauthorized'),
117
+ );
118
+ errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
119
+ stderrSpy = vi
120
+ .spyOn(process.stderr, 'write')
121
+ .mockImplementation(() => true);
122
+ exitSpy = mockExitThrow();
123
+
124
+ const { listApiKeysCommand } = await import(
125
+ '../../../src/commands/api-keys/list'
126
+ );
127
+ await expectExit1(() =>
128
+ listApiKeysCommand.parseAsync([], { from: 'user' }),
129
+ );
130
+
131
+ const output = errorSpy.mock.calls.map((c) => c[0]).join(' ');
132
+ expect(output).toContain('list_error');
133
+ });
134
+ });
@@ -0,0 +1,153 @@
1
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import {
5
+ afterEach,
6
+ beforeEach,
7
+ describe,
8
+ expect,
9
+ type MockInstance,
10
+ test,
11
+ vi,
12
+ } from 'vitest';
13
+ import {
14
+ captureTestEnv,
15
+ expectExit1,
16
+ mockExitThrow,
17
+ setupOutputSpies,
18
+ } from '../../helpers';
19
+
20
+ // Mock the Resend SDK
21
+ vi.mock('resend', () => ({
22
+ Resend: class MockResend {
23
+ constructor(public key: string) {}
24
+ domains = {
25
+ list: vi.fn(async () => ({ data: { data: [] }, error: null })),
26
+ };
27
+ },
28
+ }));
29
+
30
+ describe('login command', () => {
31
+ const restoreEnv = captureTestEnv();
32
+ let errorSpy: MockInstance | undefined;
33
+ let exitSpy: MockInstance | undefined;
34
+ let tmpDir: string;
35
+
36
+ beforeEach(() => {
37
+ tmpDir = join(
38
+ tmpdir(),
39
+ `resend-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
40
+ );
41
+ mkdirSync(tmpDir, { recursive: true });
42
+ process.env.XDG_CONFIG_HOME = tmpDir;
43
+ });
44
+
45
+ afterEach(() => {
46
+ restoreEnv();
47
+ errorSpy?.mockRestore();
48
+ errorSpy = undefined;
49
+ exitSpy?.mockRestore();
50
+ exitSpy = undefined;
51
+ rmSync(tmpDir, { recursive: true, force: true });
52
+ });
53
+
54
+ test('rejects key not starting with re_', async () => {
55
+ errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
56
+ exitSpy = mockExitThrow();
57
+
58
+ const { loginCommand } = await import('../../../src/commands/auth/login');
59
+ await expectExit1(() =>
60
+ loginCommand.parseAsync(['--key', 'bad_key'], { from: 'user' }),
61
+ );
62
+ });
63
+
64
+ test('stores valid key to credentials.json', async () => {
65
+ setupOutputSpies();
66
+
67
+ const { loginCommand } = await import('../../../src/commands/auth/login');
68
+ await loginCommand.parseAsync(['--key', 're_valid_test_key_123'], {
69
+ from: 'user',
70
+ });
71
+
72
+ const configPath = join(tmpDir, 'resend', 'credentials.json');
73
+ const data = JSON.parse(readFileSync(configPath, 'utf-8'));
74
+ expect(data.teams.default.api_key).toBe('re_valid_test_key_123');
75
+ });
76
+
77
+ test('requires --key in non-interactive mode', async () => {
78
+ setupOutputSpies();
79
+ errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
80
+ exitSpy = mockExitThrow();
81
+
82
+ const { loginCommand } = await import('../../../src/commands/auth/login');
83
+ await expectExit1(() => loginCommand.parseAsync([], { from: 'user' }));
84
+
85
+ expect(errorSpy).toBeDefined();
86
+ const output = errorSpy?.mock.calls[0][0] as string;
87
+ expect(output).toContain('missing_key');
88
+ });
89
+
90
+ test('non-interactive login stores as default when teams exist', async () => {
91
+ // Pre-populate credentials with an existing team
92
+ const configDir = join(tmpDir, 'resend');
93
+ mkdirSync(configDir, { recursive: true });
94
+ writeFileSync(
95
+ join(configDir, 'credentials.json'),
96
+ JSON.stringify({
97
+ active_team: 'production',
98
+ teams: { production: { api_key: 're_old_key_1234' } },
99
+ }),
100
+ );
101
+
102
+ setupOutputSpies();
103
+
104
+ const { loginCommand } = await import('../../../src/commands/auth/login');
105
+ await loginCommand.parseAsync(['--key', 're_new_key_5678'], {
106
+ from: 'user',
107
+ });
108
+
109
+ const configPath = join(tmpDir, 'resend', 'credentials.json');
110
+ const data = JSON.parse(readFileSync(configPath, 'utf-8'));
111
+ // Non-interactive without --team flag stores as 'default' (no picker)
112
+ expect(data.teams.default.api_key).toBe('re_new_key_5678');
113
+ // Original team should still exist
114
+ expect(data.teams.production.api_key).toBe('re_old_key_1234');
115
+ });
116
+
117
+ test('auto-switches to team specified via --team flag', async () => {
118
+ setupOutputSpies();
119
+
120
+ const { Command } = await import('@commander-js/extra-typings');
121
+ const { loginCommand } = await import('../../../src/commands/auth/login');
122
+ const program = new Command()
123
+ .option('--team <name>')
124
+ .option('--json')
125
+ .option('--api-key <key>')
126
+ .option('-q, --quiet')
127
+ .addCommand(loginCommand);
128
+
129
+ // First store a default key
130
+ const configDir = join(tmpDir, 'resend');
131
+ mkdirSync(configDir, { recursive: true });
132
+ writeFileSync(
133
+ join(configDir, 'credentials.json'),
134
+ JSON.stringify({
135
+ active_team: 'default',
136
+ teams: { default: { api_key: 're_old_key_1234' } },
137
+ }),
138
+ );
139
+
140
+ await program.parseAsync(
141
+ ['login', '--key', 're_staging_key_123', '--team', 'staging'],
142
+ { from: 'user' },
143
+ );
144
+
145
+ // @ts-expect-error — reset parent to avoid polluting the shared singleton
146
+ loginCommand.parent = null;
147
+
148
+ const configPath = join(tmpDir, 'resend', 'credentials.json');
149
+ const data = JSON.parse(readFileSync(configPath, 'utf-8'));
150
+ expect(data.active_team).toBe('staging');
151
+ expect(data.teams.staging.api_key).toBe('re_staging_key_123');
152
+ });
153
+ });
@@ -0,0 +1,153 @@
1
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import {
5
+ afterEach,
6
+ beforeEach,
7
+ describe,
8
+ expect,
9
+ type MockInstance,
10
+ test,
11
+ vi,
12
+ } from 'vitest';
13
+ import {
14
+ captureTestEnv,
15
+ expectExit1,
16
+ mockExitThrow,
17
+ setupOutputSpies,
18
+ } from '../../helpers';
19
+
20
+ describe('logout command', () => {
21
+ const restoreEnv = captureTestEnv();
22
+ let spies: ReturnType<typeof setupOutputSpies> | undefined;
23
+ let errorSpy: MockInstance | undefined;
24
+ let exitSpy: MockInstance | undefined;
25
+ let tmpDir: string;
26
+
27
+ beforeEach(() => {
28
+ tmpDir = join(
29
+ tmpdir(),
30
+ `resend-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
31
+ );
32
+ mkdirSync(tmpDir, { recursive: true });
33
+ process.env.XDG_CONFIG_HOME = tmpDir;
34
+ });
35
+
36
+ afterEach(() => {
37
+ restoreEnv();
38
+ spies = undefined;
39
+ errorSpy?.mockRestore();
40
+ errorSpy = undefined;
41
+ exitSpy?.mockRestore();
42
+ exitSpy = undefined;
43
+ rmSync(tmpDir, { recursive: true, force: true });
44
+ });
45
+
46
+ function writeCredentials(
47
+ teams: Record<string, string> = { default: 're_test_key_123' },
48
+ ) {
49
+ const configDir = join(tmpDir, 'resend');
50
+ mkdirSync(configDir, { recursive: true });
51
+ const creds = {
52
+ active_team: Object.keys(teams)[0],
53
+ teams: Object.fromEntries(
54
+ Object.entries(teams).map(([name, key]) => [name, { api_key: key }]),
55
+ ),
56
+ };
57
+ writeFileSync(
58
+ join(configDir, 'credentials.json'),
59
+ `${JSON.stringify(creds, null, 2)}\n`,
60
+ );
61
+ }
62
+
63
+ test('removes credentials file when it exists (non-interactive)', async () => {
64
+ spies = setupOutputSpies();
65
+ writeCredentials();
66
+
67
+ const { logoutCommand } = await import('../../../src/commands/auth/logout');
68
+ await logoutCommand.parseAsync([], { from: 'user' });
69
+
70
+ const configPath = join(tmpDir, 'resend', 'credentials.json');
71
+ expect(existsSync(configPath)).toBe(false);
72
+
73
+ const output = JSON.parse(spies.logSpy.mock.calls[0][0] as string);
74
+ expect(output.success).toBe(true);
75
+ expect(output.config_path).toContain('credentials.json');
76
+ });
77
+
78
+ test('exits cleanly when no credentials file exists (non-interactive)', async () => {
79
+ spies = setupOutputSpies();
80
+
81
+ const { logoutCommand } = await import('../../../src/commands/auth/logout');
82
+ await logoutCommand.parseAsync([], { from: 'user' });
83
+
84
+ const output = JSON.parse(spies.logSpy.mock.calls[0][0] as string);
85
+ expect(output.success).toBe(true);
86
+ expect(output.already_logged_out).toBe(true);
87
+ });
88
+
89
+ test('logout without --team removes all teams', async () => {
90
+ spies = setupOutputSpies();
91
+ writeCredentials({ staging: 're_staging_key', production: 're_prod_key' });
92
+
93
+ const { logoutCommand } = await import('../../../src/commands/auth/logout');
94
+ await logoutCommand.parseAsync([], { from: 'user' });
95
+
96
+ const configPath = join(tmpDir, 'resend', 'credentials.json');
97
+ expect(existsSync(configPath)).toBe(false);
98
+
99
+ const output = JSON.parse(spies.logSpy.mock.calls[0][0] as string);
100
+ expect(output.success).toBe(true);
101
+ expect(output.team).toBe('all');
102
+ });
103
+
104
+ test('logout with --team removes only that team', async () => {
105
+ spies = setupOutputSpies();
106
+ writeCredentials({ staging: 're_staging_key', production: 're_prod_key' });
107
+
108
+ // Use the full CLI program so --team global option is recognized
109
+ const { Command } = await import('@commander-js/extra-typings');
110
+ const { logoutCommand } = await import('../../../src/commands/auth/logout');
111
+ const program = new Command()
112
+ .option('--team <name>')
113
+ .option('--json')
114
+ .option('--api-key <key>')
115
+ .addCommand(logoutCommand);
116
+
117
+ await program.parseAsync(['logout', '--team', 'staging'], {
118
+ from: 'user',
119
+ });
120
+
121
+ const configPath = join(tmpDir, 'resend', 'credentials.json');
122
+ expect(existsSync(configPath)).toBe(true);
123
+
124
+ const remaining = JSON.parse(
125
+ require('node:fs').readFileSync(configPath, 'utf-8'),
126
+ );
127
+ expect(remaining.teams.staging).toBeUndefined();
128
+ expect(remaining.teams.production).toBeDefined();
129
+
130
+ const output = JSON.parse(spies.logSpy.mock.calls[0][0] as string);
131
+ expect(output.success).toBe(true);
132
+ expect(output.team).toBe('staging');
133
+ });
134
+
135
+ test('exits with error when file removal fails', async () => {
136
+ errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
137
+ exitSpy = mockExitThrow();
138
+ spies = setupOutputSpies();
139
+ writeCredentials();
140
+
141
+ // Make the credentials file a directory so unlinkSync throws
142
+ const configPath = join(tmpDir, 'resend', 'credentials.json');
143
+ rmSync(configPath);
144
+ mkdirSync(configPath); // replace file with a directory — unlinkSync will throw EISDIR
145
+
146
+ const { logoutCommand } = await import('../../../src/commands/auth/logout');
147
+ await expectExit1(() => logoutCommand.parseAsync([], { from: 'user' }));
148
+
149
+ expect(errorSpy).toBeDefined();
150
+ const output = JSON.parse(errorSpy?.mock.calls[0][0] as string);
151
+ expect(output.error.code).toBe('remove_failed');
152
+ });
153
+ });