@meltstudio/meltctl 4.35.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.
- package/dist/commands/audit.js +66 -111
- package/dist/commands/audit.test.js +118 -209
- package/dist/commands/coins.js +30 -28
- package/dist/commands/coins.test.js +23 -43
- package/dist/commands/feedback.js +8 -17
- package/dist/commands/feedback.test.js +38 -103
- package/dist/commands/login.js +15 -20
- package/dist/commands/plan.js +21 -51
- package/dist/commands/plan.test.js +95 -132
- package/dist/commands/standup.js +10 -14
- package/dist/commands/standup.test.js +66 -100
- package/dist/utils/analytics.js +9 -19
- package/dist/utils/api.d.ts +2 -1
- package/dist/utils/api.js +4 -12
- package/dist/utils/api.test.js +25 -45
- package/dist/utils/templates.d.ts +2 -4
- package/dist/utils/templates.js +3 -7
- package/dist/utils/templates.test.js +14 -26
- package/package.json +3 -2
|
@@ -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
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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(
|
|
56
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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(
|
|
71
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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(
|
|
86
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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(
|
|
101
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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(
|
|
158
|
-
|
|
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(
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
199
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
|
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);
|
package/dist/commands/coins.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { isAuthenticated
|
|
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
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
console.
|
|
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
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
}
|