@meltstudio/meltctl 4.34.0 → 4.36.0

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.
@@ -7,9 +7,15 @@ vi.mock('fs-extra', () => ({
7
7
  writeFile: vi.fn(),
8
8
  },
9
9
  }));
10
+ const mockClient = vi.hoisted(() => ({
11
+ audits: {
12
+ submit: vi.fn(),
13
+ list: vi.fn(),
14
+ get: vi.fn(),
15
+ },
16
+ }));
10
17
  vi.mock('../utils/api.js', () => ({
11
- getToken: vi.fn(),
12
- tokenFetch: vi.fn(),
18
+ getClient: vi.fn().mockResolvedValue(mockClient),
13
19
  }));
14
20
  vi.mock('../utils/git.js', () => ({
15
21
  getGitBranch: vi.fn(),
@@ -19,7 +25,6 @@ vi.mock('../utils/git.js', () => ({
19
25
  findMdFiles: vi.fn(),
20
26
  }));
21
27
  import fs from 'fs-extra';
22
- import { getToken, tokenFetch } from '../utils/api.js';
23
28
  import { getGitBranch, getGitCommit, getGitRepository, getProjectName, findMdFiles, } from '../utils/git.js';
24
29
  import { auditSubmitCommand, auditListCommand, auditViewCommand } from './audit.js';
25
30
  beforeEach(() => {
@@ -33,7 +38,6 @@ beforeEach(() => {
33
38
  describe('auditSubmitCommand', () => {
34
39
  function setupGitMocks() {
35
40
  ;
36
- getToken.mockResolvedValue('test-token');
37
41
  getGitBranch.mockReturnValue('main');
38
42
  getGitCommit.mockReturnValue('abc1234');
39
43
  getGitRepository.mockReturnValue({
@@ -46,60 +50,52 @@ describe('auditSubmitCommand', () => {
46
50
  setupGitMocks();
47
51
  fs.pathExists.mockResolvedValue(true);
48
52
  fs.readFile.mockResolvedValue('# Security Audit\nFindings here.');
49
- const mockResponse = {
50
- ok: true,
51
- json: vi.fn().mockResolvedValue({ id: 'audit-123' }),
52
- };
53
- tokenFetch.mockResolvedValue(mockResponse);
53
+ mockClient.audits.submit.mockResolvedValue({
54
+ id: 'audit-123',
55
+ createdAt: '2026-03-26T00:00:00Z',
56
+ });
54
57
  await auditSubmitCommand('2026-03-26-security-audit.md');
55
- expect(tokenFetch).toHaveBeenCalledWith('test-token', '/audits', expect.objectContaining({
56
- method: 'POST',
57
- body: expect.stringContaining('"type":"security-audit"'),
58
+ expect(mockClient.audits.submit).toHaveBeenCalledWith(expect.objectContaining({
59
+ type: 'security-audit',
58
60
  }));
59
61
  });
60
62
  it('submits audit with type "ux-audit" when filename contains ux-audit', async () => {
61
63
  setupGitMocks();
62
64
  fs.pathExists.mockResolvedValue(true);
63
65
  fs.readFile.mockResolvedValue('# UX Audit');
64
- const mockResponse = {
65
- ok: true,
66
- json: vi.fn().mockResolvedValue({ id: 'audit-456' }),
67
- };
68
- tokenFetch.mockResolvedValue(mockResponse);
66
+ mockClient.audits.submit.mockResolvedValue({
67
+ id: 'audit-456',
68
+ createdAt: '2026-03-26T00:00:00Z',
69
+ });
69
70
  await auditSubmitCommand('UX-AUDIT.md');
70
- expect(tokenFetch).toHaveBeenCalledWith('test-token', '/audits', expect.objectContaining({
71
- method: 'POST',
72
- body: expect.stringContaining('"type":"ux-audit"'),
71
+ expect(mockClient.audits.submit).toHaveBeenCalledWith(expect.objectContaining({
72
+ type: 'ux-audit',
73
73
  }));
74
74
  });
75
75
  it('submits audit with type "audit" for generic audit filenames', async () => {
76
76
  setupGitMocks();
77
77
  fs.pathExists.mockResolvedValue(true);
78
78
  fs.readFile.mockResolvedValue('# Audit');
79
- const mockResponse = {
80
- ok: true,
81
- json: vi.fn().mockResolvedValue({ id: 'audit-789' }),
82
- };
83
- tokenFetch.mockResolvedValue(mockResponse);
79
+ mockClient.audits.submit.mockResolvedValue({
80
+ id: 'audit-789',
81
+ createdAt: '2026-03-26T00:00:00Z',
82
+ });
84
83
  await auditSubmitCommand('AUDIT.md');
85
- expect(tokenFetch).toHaveBeenCalledWith('test-token', '/audits', expect.objectContaining({
86
- method: 'POST',
87
- body: expect.stringContaining('"type":"audit"'),
84
+ expect(mockClient.audits.submit).toHaveBeenCalledWith(expect.objectContaining({
85
+ type: 'audit',
88
86
  }));
89
87
  });
90
88
  it('submits audit with type "audit" for random filenames', async () => {
91
89
  setupGitMocks();
92
90
  fs.pathExists.mockResolvedValue(true);
93
91
  fs.readFile.mockResolvedValue('# Random');
94
- const mockResponse = {
95
- ok: true,
96
- json: vi.fn().mockResolvedValue({ id: 'audit-000' }),
97
- };
98
- tokenFetch.mockResolvedValue(mockResponse);
92
+ mockClient.audits.submit.mockResolvedValue({
93
+ id: 'audit-000',
94
+ createdAt: '2026-03-26T00:00:00Z',
95
+ });
99
96
  await auditSubmitCommand('random-file.md');
100
- expect(tokenFetch).toHaveBeenCalledWith('test-token', '/audits', expect.objectContaining({
101
- method: 'POST',
102
- body: expect.stringContaining('"type":"audit"'),
97
+ expect(mockClient.audits.submit).toHaveBeenCalledWith(expect.objectContaining({
98
+ type: 'audit',
103
99
  }));
104
100
  });
105
101
  it('exits with error when file not found', async () => {
@@ -112,15 +108,12 @@ describe('auditSubmitCommand', () => {
112
108
  setupGitMocks();
113
109
  vi.mocked(fs.pathExists).mockResolvedValue(true);
114
110
  vi.mocked(fs.readFile).mockResolvedValue('audit content here');
115
- const mockResponse = {
116
- ok: true,
117
- json: vi.fn().mockResolvedValue({ id: 'audit-100' }),
118
- };
119
- tokenFetch.mockResolvedValue(mockResponse);
111
+ mockClient.audits.submit.mockResolvedValue({
112
+ id: 'audit-100',
113
+ createdAt: '2026-03-26T00:00:00Z',
114
+ });
120
115
  await auditSubmitCommand('AUDIT.md');
121
- const call = tokenFetch.mock.calls[0];
122
- const body = JSON.parse(call[2].body);
123
- expect(body).toEqual({
116
+ expect(mockClient.audits.submit).toHaveBeenCalledWith({
124
117
  type: 'audit',
125
118
  project: 'test-project',
126
119
  repository: 'Org/Repo',
@@ -135,12 +128,7 @@ describe('auditSubmitCommand', () => {
135
128
  setupGitMocks();
136
129
  fs.pathExists.mockResolvedValue(true);
137
130
  fs.readFile.mockResolvedValue('content');
138
- const mockResponse = {
139
- ok: false,
140
- statusText: 'Bad Request',
141
- json: vi.fn().mockResolvedValue({ error: 'Invalid content' }),
142
- };
143
- tokenFetch.mockResolvedValue(mockResponse);
131
+ mockClient.audits.submit.mockRejectedValue(new Error('Invalid content'));
144
132
  await expect(auditSubmitCommand('AUDIT.md')).rejects.toThrow('process.exit(1)');
145
133
  });
146
134
  it('auto-detects audit file from .audits/ directory when no file provided', async () => {
@@ -148,20 +136,17 @@ describe('auditSubmitCommand', () => {
148
136
  findMdFiles.mockResolvedValue(['/project/.audits/2026-03-26-security-audit.md']);
149
137
  fs.pathExists.mockResolvedValue(true);
150
138
  fs.readFile.mockResolvedValue('auto-detected content');
151
- const mockResponse = {
152
- ok: true,
153
- json: vi.fn().mockResolvedValue({ id: 'audit-auto' }),
154
- };
155
- tokenFetch.mockResolvedValue(mockResponse);
139
+ mockClient.audits.submit.mockResolvedValue({
140
+ id: 'audit-auto',
141
+ createdAt: '2026-03-26T00:00:00Z',
142
+ });
156
143
  await auditSubmitCommand();
157
- expect(tokenFetch).toHaveBeenCalledWith('test-token', '/audits', expect.objectContaining({
158
- method: 'POST',
159
- body: expect.stringContaining('"type":"security-audit"'),
144
+ expect(mockClient.audits.submit).toHaveBeenCalledWith(expect.objectContaining({
145
+ type: 'security-audit',
160
146
  }));
161
147
  });
162
148
  it('exits with error when no file provided and none auto-detected', async () => {
163
149
  ;
164
- getToken.mockResolvedValue('test-token');
165
150
  findMdFiles.mockResolvedValue([]);
166
151
  fs.pathExists.mockResolvedValue(false);
167
152
  await expect(auditSubmitCommand()).rejects.toThrow('process.exit(1)');
@@ -170,122 +155,89 @@ describe('auditSubmitCommand', () => {
170
155
  });
171
156
  describe('auditListCommand', () => {
172
157
  it('calls API with correct query params', async () => {
173
- ;
174
- getToken.mockResolvedValue('test-token');
175
- const mockResponse = {
176
- ok: true,
177
- status: 200,
178
- json: vi.fn().mockResolvedValue({ audits: [], count: 0 }),
179
- };
180
- tokenFetch.mockResolvedValue(mockResponse);
158
+ mockClient.audits.list.mockResolvedValue({ audits: [], count: 0 });
181
159
  await auditListCommand({ type: 'ux-audit', repository: 'Org/Repo', limit: '5' });
182
- expect(tokenFetch).toHaveBeenCalledWith('test-token', expect.stringContaining('/audits?'));
183
- const url = tokenFetch.mock.calls[0][1];
184
- expect(url).toContain('type=ux-audit');
185
- expect(url).toContain('repository=Org%2FRepo');
186
- expect(url).toContain('limit=5');
160
+ expect(mockClient.audits.list).toHaveBeenCalledWith({
161
+ type: 'ux-audit',
162
+ repository: 'Org/Repo',
163
+ latest: undefined,
164
+ limit: 5,
165
+ });
187
166
  });
188
167
  it('passes latest=true query param when option set', async () => {
189
- ;
190
- getToken.mockResolvedValue('test-token');
191
- const mockResponse = {
192
- ok: true,
193
- status: 200,
194
- json: vi.fn().mockResolvedValue({ audits: [], count: 0 }),
195
- };
196
- tokenFetch.mockResolvedValue(mockResponse);
168
+ mockClient.audits.list.mockResolvedValue({ audits: [], count: 0 });
197
169
  await auditListCommand({ latest: true });
198
- const url = tokenFetch.mock.calls[0][1];
199
- expect(url).toContain('latest=true');
170
+ expect(mockClient.audits.list).toHaveBeenCalledWith(expect.objectContaining({
171
+ latest: true,
172
+ }));
200
173
  });
201
174
  it('exits with error on 403 response', async () => {
202
- ;
203
- getToken.mockResolvedValue('test-token');
204
- const mockResponse = {
205
- ok: false,
206
- status: 403,
207
- statusText: 'Forbidden',
208
- };
209
- tokenFetch.mockResolvedValue(mockResponse);
175
+ mockClient.audits.list.mockRejectedValue(new Error('Access denied. Only Team Managers can list audits.'));
210
176
  await expect(auditListCommand({})).rejects.toThrow('process.exit(1)');
211
177
  expect(console.error).toHaveBeenCalled();
212
178
  });
213
179
  it('displays audit list when audits exist', async () => {
214
- ;
215
- getToken.mockResolvedValue('test-token');
216
- const mockResponse = {
217
- ok: true,
218
- status: 200,
219
- json: vi.fn().mockResolvedValue({
220
- audits: [
221
- {
222
- id: '1',
223
- type: 'audit',
224
- project: 'my-project',
225
- repository: 'Org/Repo',
226
- author: 'dev@meltstudio.co',
227
- branch: 'main',
228
- commit: 'abc1234',
229
- createdAt: '2026-03-25T10:00:00Z',
230
- },
231
- ],
232
- count: 1,
233
- }),
234
- };
235
- tokenFetch.mockResolvedValue(mockResponse);
180
+ mockClient.audits.list.mockResolvedValue({
181
+ audits: [
182
+ {
183
+ id: '1',
184
+ type: 'audit',
185
+ project: 'my-project',
186
+ repository: 'Org/Repo',
187
+ author: 'dev@meltstudio.co',
188
+ branch: 'main',
189
+ commit: 'abc1234',
190
+ createdAt: '2026-03-25T10:00:00Z',
191
+ },
192
+ ],
193
+ count: 1,
194
+ });
236
195
  await auditListCommand({});
237
196
  expect(console.log).toHaveBeenCalled();
238
197
  });
239
198
  it('displays age-colored output when latest option is set', async () => {
240
- ;
241
- getToken.mockResolvedValue('test-token');
242
199
  const now = Date.now();
243
200
  const threeDaysAgo = new Date(now - 3 * 24 * 60 * 60 * 1000).toISOString();
244
201
  const fifteenDaysAgo = new Date(now - 15 * 24 * 60 * 60 * 1000).toISOString();
245
202
  const sixtyDaysAgo = new Date(now - 60 * 24 * 60 * 60 * 1000).toISOString();
246
- const mockResponse = {
247
- ok: true,
248
- status: 200,
249
- json: vi.fn().mockResolvedValue({
250
- audits: [
251
- {
252
- id: '1',
253
- type: 'audit',
254
- project: 'project-a',
255
- repository: 'Org/RepoA',
256
- author: 'dev1@meltstudio.co',
257
- branch: 'main',
258
- commit: 'aaa1111',
259
- createdAt: threeDaysAgo,
260
- created_at: threeDaysAgo,
261
- },
262
- {
263
- id: '2',
264
- type: 'ux-audit',
265
- project: 'project-b',
266
- repository: 'Org/RepoB',
267
- author: 'dev2@meltstudio.co',
268
- branch: 'main',
269
- commit: 'bbb2222',
270
- createdAt: fifteenDaysAgo,
271
- created_at: fifteenDaysAgo,
272
- },
273
- {
274
- id: '3',
275
- type: 'security-audit',
276
- project: 'project-c',
277
- repository: 'Org/RepoC',
278
- author: 'dev3@meltstudio.co',
279
- branch: 'main',
280
- commit: 'ccc3333',
281
- createdAt: sixtyDaysAgo,
282
- created_at: sixtyDaysAgo,
283
- },
284
- ],
285
- count: 3,
286
- }),
287
- };
288
- tokenFetch.mockResolvedValue(mockResponse);
203
+ mockClient.audits.list.mockResolvedValue({
204
+ audits: [
205
+ {
206
+ id: '1',
207
+ type: 'audit',
208
+ project: 'project-a',
209
+ repository: 'Org/RepoA',
210
+ author: 'dev1@meltstudio.co',
211
+ branch: 'main',
212
+ commit: 'aaa1111',
213
+ createdAt: threeDaysAgo,
214
+ created_at: threeDaysAgo,
215
+ },
216
+ {
217
+ id: '2',
218
+ type: 'ux-audit',
219
+ project: 'project-b',
220
+ repository: 'Org/RepoB',
221
+ author: 'dev2@meltstudio.co',
222
+ branch: 'main',
223
+ commit: 'bbb2222',
224
+ createdAt: fifteenDaysAgo,
225
+ created_at: fifteenDaysAgo,
226
+ },
227
+ {
228
+ id: '3',
229
+ type: 'security-audit',
230
+ project: 'project-c',
231
+ repository: 'Org/RepoC',
232
+ author: 'dev3@meltstudio.co',
233
+ branch: 'main',
234
+ commit: 'ccc3333',
235
+ createdAt: sixtyDaysAgo,
236
+ created_at: sixtyDaysAgo,
237
+ },
238
+ ],
239
+ count: 3,
240
+ });
289
241
  await auditListCommand({ latest: true });
290
242
  const logCalls = console.log.mock.calls.map((c) => String(c[0]));
291
243
  // Should display "Latest Audits" header
@@ -293,30 +245,20 @@ describe('auditListCommand', () => {
293
245
  // Should display AGE column header
294
246
  expect(logCalls.some((msg) => msg.includes('AGE'))).toBe(true);
295
247
  });
296
- it('exits with error when auditListCommand tokenFetch throws', async () => {
297
- ;
298
- getToken.mockResolvedValue('test-token');
299
- tokenFetch.mockRejectedValue(new Error('Network error'));
248
+ it('exits with error when auditListCommand throws', async () => {
249
+ mockClient.audits.list.mockRejectedValue(new Error('Network error'));
300
250
  await expect(auditListCommand({})).rejects.toThrow('process.exit(1)');
301
251
  const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
302
252
  expect(errorCalls.some((msg) => msg.includes('Network error'))).toBe(true);
303
253
  });
304
254
  it('exits with error when findMdFiles throws (e.g. .audits/ unreadable)', async () => {
305
255
  ;
306
- getToken.mockResolvedValue('test-token');
307
256
  findMdFiles.mockRejectedValue(new Error('EACCES: permission denied'));
308
257
  fs.pathExists.mockResolvedValue(false);
309
258
  await expect(auditSubmitCommand()).rejects.toThrow();
310
259
  });
311
260
  it('shows "No audits found" when list is empty', async () => {
312
- ;
313
- getToken.mockResolvedValue('test-token');
314
- const mockResponse = {
315
- ok: true,
316
- status: 200,
317
- json: vi.fn().mockResolvedValue({ audits: [], count: 0 }),
318
- };
319
- tokenFetch.mockResolvedValue(mockResponse);
261
+ mockClient.audits.list.mockResolvedValue({ audits: [], count: 0 });
320
262
  await auditListCommand({});
321
263
  const errorCalls = console.log.mock.calls.map((c) => c[0]);
322
264
  expect(errorCalls.some((msg) => typeof msg === 'string' && msg.includes('No audits found'))).toBe(true);
@@ -335,50 +277,25 @@ describe('auditViewCommand', () => {
335
277
  createdAt: '2026-03-25T10:00:00Z',
336
278
  };
337
279
  it('exits with "Access denied" on 403 response', async () => {
338
- ;
339
- getToken.mockResolvedValue('test-token');
340
- tokenFetch.mockResolvedValue({
341
- ok: false,
342
- status: 403,
343
- statusText: 'Forbidden',
344
- });
280
+ mockClient.audits.get.mockRejectedValue(new Error('Access denied. Only Team Managers can list audits.'));
345
281
  await expect(auditViewCommand('audit-abc123', {})).rejects.toThrow('process.exit(1)');
346
282
  const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
347
283
  expect(errorCalls.some((msg) => msg.includes('Access denied'))).toBe(true);
348
284
  });
349
285
  it('exits with "Audit not found" on 404 response', async () => {
350
- ;
351
- getToken.mockResolvedValue('test-token');
352
- tokenFetch.mockResolvedValue({
353
- ok: false,
354
- status: 404,
355
- statusText: 'Not Found',
356
- });
286
+ mockClient.audits.get.mockRejectedValue(new Error('Audit not found: nonexistent-id'));
357
287
  await expect(auditViewCommand('nonexistent-id', {})).rejects.toThrow('process.exit(1)');
358
288
  const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
359
289
  expect(errorCalls.some((msg) => msg.includes('Audit not found'))).toBe(true);
360
290
  });
361
291
  it('exits with error from body on non-ok response', async () => {
362
- ;
363
- getToken.mockResolvedValue('test-token');
364
- tokenFetch.mockResolvedValue({
365
- ok: false,
366
- status: 500,
367
- statusText: 'Internal Server Error',
368
- json: vi.fn().mockResolvedValue({ error: 'Database unavailable' }),
369
- });
292
+ mockClient.audits.get.mockRejectedValue(new Error('Database unavailable'));
370
293
  await expect(auditViewCommand('audit-abc123', {})).rejects.toThrow('process.exit(1)');
371
294
  const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
372
295
  expect(errorCalls.some((msg) => msg.includes('Database unavailable'))).toBe(true);
373
296
  });
374
297
  it('prints audit content with header when no --output flag', async () => {
375
- ;
376
- getToken.mockResolvedValue('test-token');
377
- tokenFetch.mockResolvedValue({
378
- ok: true,
379
- status: 200,
380
- json: vi.fn().mockResolvedValue(sampleAudit),
381
- });
298
+ mockClient.audits.get.mockResolvedValue(sampleAudit);
382
299
  await auditViewCommand('audit-abc123', {});
383
300
  const logCalls = console.log.mock.calls.map((c) => String(c[0]));
384
301
  // Header should contain type label, repo, author, and date
@@ -389,13 +306,7 @@ describe('auditViewCommand', () => {
389
306
  expect(logCalls.some((msg) => msg.includes('# UX Audit'))).toBe(true);
390
307
  });
391
308
  it('writes content to file with --output flag', async () => {
392
- ;
393
- getToken.mockResolvedValue('test-token');
394
- tokenFetch.mockResolvedValue({
395
- ok: true,
396
- status: 200,
397
- json: vi.fn().mockResolvedValue(sampleAudit),
398
- });
309
+ mockClient.audits.get.mockResolvedValue(sampleAudit);
399
310
  fs.ensureDir.mockResolvedValue(undefined);
400
311
  fs.writeFile.mockResolvedValue(undefined);
401
312
  await auditViewCommand('audit-abc123', { output: 'output/audit.md' });
@@ -405,9 +316,7 @@ describe('auditViewCommand', () => {
405
316
  expect(logCalls.some((msg) => msg.includes('Audit saved to'))).toBe(true);
406
317
  });
407
318
  it('exits with error message on network error', async () => {
408
- ;
409
- getToken.mockResolvedValue('test-token');
410
- tokenFetch.mockRejectedValue(new Error('Network error'));
319
+ mockClient.audits.get.mockRejectedValue(new Error('Network error'));
411
320
  await expect(auditViewCommand('audit-abc123', {})).rejects.toThrow('process.exit(1)');
412
321
  const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
413
322
  expect(errorCalls.some((msg) => msg.includes('Network error'))).toBe(true);
@@ -1,5 +1,6 @@
1
1
  import chalk from 'chalk';
2
- import { isAuthenticated, authenticatedFetch } from '../utils/auth.js';
2
+ import { isAuthenticated } from '../utils/auth.js';
3
+ import { getClient } from '../utils/api.js';
3
4
  export async function coinsCommand(options) {
4
5
  if (!(await isAuthenticated())) {
5
6
  console.error(chalk.red('Not authenticated. Run `npx @meltstudio/meltctl@latest login` first.'));
@@ -13,37 +14,38 @@ export async function coinsCommand(options) {
13
14
  }
14
15
  }
15
16
  async function showBalance() {
16
- const res = await authenticatedFetch('/coins');
17
- if (!res.ok) {
18
- const body = (await res.json());
19
- console.error(chalk.red(`Failed to fetch coins: ${body.error ?? res.statusText}`));
17
+ const client = await getClient();
18
+ try {
19
+ const data = await client.coins.getBalance();
20
+ console.log(chalk.bold.cyan('\n Your Coins (last 28 days)'));
21
+ console.log(` ${chalk.bold(String(data.coins))} coin${data.coins !== 1 ? 's' : ''} received\n`);
22
+ }
23
+ catch (error) {
24
+ console.error(chalk.red(`Failed to fetch coins: ${error instanceof Error ? error.message : 'Unknown error'}`));
20
25
  process.exit(1);
21
26
  }
22
- const data = (await res.json());
23
- console.log(chalk.bold.cyan('\n Your Coins (last 28 days)'));
24
- console.log(` ${chalk.bold(String(data.coins))} coin${data.coins !== 1 ? 's' : ''} received\n`);
25
27
  }
26
28
  async function showLeaderboard() {
27
- const res = await authenticatedFetch('/coins/leaderboard');
28
- if (!res.ok) {
29
- const body = (await res.json());
30
- console.error(chalk.red(`Failed to fetch leaderboard: ${body.error ?? res.statusText}`));
31
- process.exit(1);
29
+ const client = await getClient();
30
+ try {
31
+ const entries = await client.coins.getLeaderboard();
32
+ if (entries.length === 0) {
33
+ console.log(chalk.yellow('\n No coins have been sent in the last 28 days.\n'));
34
+ return;
35
+ }
36
+ console.log(chalk.bold.cyan('\n Leaderboard (last 28 days)\n'));
37
+ const maxNameLen = Math.max(...entries.map(e => e.name.length), 4);
38
+ console.log(chalk.dim(` ${'#'.padEnd(4)} ${'Name'.padEnd(maxNameLen)} Coins`));
39
+ entries.forEach((entry, i) => {
40
+ const rank = String(i + 1).padEnd(4);
41
+ const name = entry.name.padEnd(maxNameLen);
42
+ const coins = String(entry.coins);
43
+ console.log(` ${rank} ${name} ${coins}`);
44
+ });
45
+ console.log();
32
46
  }
33
- const entries = (await res.json());
34
- if (entries.length === 0) {
35
- console.log(chalk.yellow('\n No coins have been sent in the last 28 days.\n'));
36
- return;
47
+ catch (error) {
48
+ console.error(chalk.red(`Failed to fetch leaderboard: ${error instanceof Error ? error.message : 'Unknown error'}`));
49
+ process.exit(1);
37
50
  }
38
- console.log(chalk.bold.cyan('\n Leaderboard (last 28 days)\n'));
39
- // Calculate column widths
40
- const maxNameLen = Math.max(...entries.map(e => e.name.length), 4);
41
- console.log(chalk.dim(` ${'#'.padEnd(4)} ${'Name'.padEnd(maxNameLen)} Coins`));
42
- entries.forEach((entry, i) => {
43
- const rank = String(i + 1).padEnd(4);
44
- const name = entry.name.padEnd(maxNameLen);
45
- const coins = String(entry.coins);
46
- console.log(` ${rank} ${name} ${coins}`);
47
- });
48
- console.log();
49
51
  }