@meltstudio/meltctl 4.35.0 → 4.36.1
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
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
2
|
vi.mock('../utils/auth.js', () => ({
|
|
3
3
|
isAuthenticated: vi.fn(),
|
|
4
|
-
|
|
4
|
+
}));
|
|
5
|
+
const mockClient = vi.hoisted(() => ({
|
|
6
|
+
standups: {
|
|
7
|
+
getStatus: vi.fn(),
|
|
8
|
+
submit: vi.fn(),
|
|
9
|
+
},
|
|
10
|
+
}));
|
|
11
|
+
vi.mock('../utils/api.js', () => ({
|
|
12
|
+
getClient: vi.fn().mockResolvedValue(mockClient),
|
|
5
13
|
}));
|
|
6
14
|
vi.mock('@inquirer/prompts', () => ({
|
|
7
15
|
input: vi.fn(),
|
|
8
16
|
editor: vi.fn(),
|
|
9
17
|
}));
|
|
10
|
-
import { isAuthenticated
|
|
18
|
+
import { isAuthenticated } from '../utils/auth.js';
|
|
11
19
|
import { input, editor } from '@inquirer/prompts';
|
|
12
20
|
import { standupCommand } from './standup.js';
|
|
13
21
|
beforeEach(() => {
|
|
@@ -28,47 +36,33 @@ describe('standupCommand', () => {
|
|
|
28
36
|
it('returns early when standup already submitted today', async () => {
|
|
29
37
|
;
|
|
30
38
|
isAuthenticated.mockResolvedValue(true);
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
blockers: null,
|
|
37
|
-
}),
|
|
38
|
-
};
|
|
39
|
-
authenticatedFetch.mockResolvedValue(statusResponse);
|
|
39
|
+
mockClient.standups.getStatus.mockResolvedValue({
|
|
40
|
+
yesterday: 'Did stuff',
|
|
41
|
+
today: 'More stuff',
|
|
42
|
+
blockers: null,
|
|
43
|
+
});
|
|
40
44
|
await standupCommand({});
|
|
41
|
-
expect(
|
|
42
|
-
// Should not call
|
|
43
|
-
expect(
|
|
45
|
+
expect(mockClient.standups.getStatus).toHaveBeenCalled();
|
|
46
|
+
// Should not call submit
|
|
47
|
+
expect(mockClient.standups.submit).not.toHaveBeenCalled();
|
|
44
48
|
const logCalls = console.log.mock.calls.map((c) => String(c[0]));
|
|
45
49
|
expect(logCalls.some((msg) => msg.includes('already submitted'))).toBe(true);
|
|
46
50
|
});
|
|
47
51
|
it('submits standup with correct payload using option flags', async () => {
|
|
48
52
|
;
|
|
49
53
|
isAuthenticated.mockResolvedValue(true);
|
|
50
|
-
// Status check returns
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
ok: true,
|
|
54
|
-
json: vi.fn().mockResolvedValue({}),
|
|
55
|
-
};
|
|
56
|
-
authenticatedFetch
|
|
57
|
-
.mockResolvedValueOnce(statusResponse)
|
|
58
|
-
.mockResolvedValueOnce(submitResponse);
|
|
54
|
+
// Status check returns null (no existing standup)
|
|
55
|
+
mockClient.standups.getStatus.mockResolvedValue(null);
|
|
56
|
+
mockClient.standups.submit.mockResolvedValue({});
|
|
59
57
|
await standupCommand({
|
|
60
58
|
yesterday: 'Fixed bugs',
|
|
61
59
|
today: 'Write tests',
|
|
62
60
|
blockers: 'None',
|
|
63
61
|
});
|
|
64
|
-
expect(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
yesterday: 'Fixed bugs',
|
|
69
|
-
today: 'Write tests',
|
|
70
|
-
blockers: 'None',
|
|
71
|
-
}),
|
|
62
|
+
expect(mockClient.standups.submit).toHaveBeenCalledWith({
|
|
63
|
+
yesterday: 'Fixed bugs',
|
|
64
|
+
today: 'Write tests',
|
|
65
|
+
blockers: 'None',
|
|
72
66
|
});
|
|
73
67
|
const logCalls = console.log.mock.calls.map((c) => String(c[0]));
|
|
74
68
|
expect(logCalls.some((msg) => msg.includes('Standup submitted'))).toBe(true);
|
|
@@ -76,58 +70,45 @@ describe('standupCommand', () => {
|
|
|
76
70
|
it('submits standup without blockers when empty string', async () => {
|
|
77
71
|
;
|
|
78
72
|
isAuthenticated.mockResolvedValue(true);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
authenticatedFetch
|
|
82
|
-
.mockResolvedValueOnce(statusResponse)
|
|
83
|
-
.mockResolvedValueOnce(submitResponse);
|
|
73
|
+
mockClient.standups.getStatus.mockResolvedValue(null);
|
|
74
|
+
mockClient.standups.submit.mockResolvedValue({});
|
|
84
75
|
await standupCommand({
|
|
85
76
|
yesterday: 'Did things',
|
|
86
77
|
today: 'More things',
|
|
87
78
|
});
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
79
|
+
expect(mockClient.standups.submit).toHaveBeenCalledWith({
|
|
80
|
+
yesterday: 'Did things',
|
|
81
|
+
today: 'More things',
|
|
82
|
+
blockers: undefined,
|
|
83
|
+
});
|
|
91
84
|
});
|
|
92
85
|
it('exits with error when API returns failure', async () => {
|
|
93
86
|
;
|
|
94
87
|
isAuthenticated.mockResolvedValue(true);
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
ok: false,
|
|
98
|
-
statusText: 'Bad Request',
|
|
99
|
-
json: vi.fn().mockResolvedValue({ error: 'Invalid standup' }),
|
|
100
|
-
};
|
|
101
|
-
authenticatedFetch
|
|
102
|
-
.mockResolvedValueOnce(statusResponse)
|
|
103
|
-
.mockResolvedValueOnce(submitResponse);
|
|
88
|
+
mockClient.standups.getStatus.mockResolvedValue(null);
|
|
89
|
+
mockClient.standups.submit.mockRejectedValue(new Error('Invalid standup'));
|
|
104
90
|
await expect(standupCommand({ yesterday: 'x', today: 'y' })).rejects.toThrow('process.exit(1)');
|
|
105
91
|
expect(console.error).toHaveBeenCalled();
|
|
106
92
|
});
|
|
107
93
|
it('continues to submission when status check throws', async () => {
|
|
108
94
|
;
|
|
109
95
|
isAuthenticated.mockResolvedValue(true);
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
.mockResolvedValueOnce({ ok: true, json: vi.fn().mockResolvedValue({}) });
|
|
96
|
+
mockClient.standups.getStatus.mockRejectedValue(new Error('Network error'));
|
|
97
|
+
mockClient.standups.submit.mockResolvedValue({});
|
|
113
98
|
await standupCommand({ yesterday: 'a', today: 'b' });
|
|
114
|
-
expect(
|
|
115
|
-
|
|
116
|
-
|
|
99
|
+
expect(mockClient.standups.submit).toHaveBeenCalledWith(expect.objectContaining({
|
|
100
|
+
yesterday: 'a',
|
|
101
|
+
today: 'b',
|
|
117
102
|
}));
|
|
118
103
|
});
|
|
119
104
|
it('displays existing standup with blockers', async () => {
|
|
120
105
|
;
|
|
121
106
|
isAuthenticated.mockResolvedValue(true);
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
blockers: 'Waiting on review',
|
|
128
|
-
}),
|
|
129
|
-
};
|
|
130
|
-
authenticatedFetch.mockResolvedValue(statusResponse);
|
|
107
|
+
mockClient.standups.getStatus.mockResolvedValue({
|
|
108
|
+
yesterday: 'Bug fixes',
|
|
109
|
+
today: 'Feature work',
|
|
110
|
+
blockers: 'Waiting on review',
|
|
111
|
+
});
|
|
131
112
|
await standupCommand({});
|
|
132
113
|
const logCalls = console.log.mock.calls.map((c) => String(c[0]));
|
|
133
114
|
expect(logCalls.some((msg) => msg.includes('already submitted'))).toBe(true);
|
|
@@ -136,44 +117,41 @@ describe('standupCommand', () => {
|
|
|
136
117
|
function setupInteractiveAuth() {
|
|
137
118
|
;
|
|
138
119
|
isAuthenticated.mockResolvedValue(true);
|
|
139
|
-
|
|
120
|
+
// Status check returns null (no existing standup)
|
|
121
|
+
mockClient.standups.getStatus.mockResolvedValue(null);
|
|
140
122
|
}
|
|
141
123
|
it('prompts for yesterday, today, and blockers in interactive mode', async () => {
|
|
142
124
|
setupInteractiveAuth();
|
|
143
|
-
|
|
144
|
-
authenticatedFetch.mockResolvedValueOnce(submitResponse);
|
|
125
|
+
mockClient.standups.submit.mockResolvedValue({});
|
|
145
126
|
input
|
|
146
127
|
.mockResolvedValueOnce('Worked on feature X')
|
|
147
128
|
.mockResolvedValueOnce('Working on feature Y')
|
|
148
129
|
.mockResolvedValueOnce('No blockers');
|
|
149
130
|
await standupCommand({});
|
|
150
131
|
expect(input).toHaveBeenCalledTimes(3);
|
|
151
|
-
expect(
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
blockers: 'No blockers',
|
|
157
|
-
}),
|
|
158
|
-
}));
|
|
132
|
+
expect(mockClient.standups.submit).toHaveBeenCalledWith({
|
|
133
|
+
yesterday: 'Worked on feature X',
|
|
134
|
+
today: 'Working on feature Y',
|
|
135
|
+
blockers: 'No blockers',
|
|
136
|
+
});
|
|
159
137
|
});
|
|
160
138
|
it('submits without blockers when left empty in interactive mode', async () => {
|
|
161
139
|
setupInteractiveAuth();
|
|
162
|
-
|
|
163
|
-
authenticatedFetch.mockResolvedValueOnce(submitResponse);
|
|
140
|
+
mockClient.standups.submit.mockResolvedValue({});
|
|
164
141
|
input
|
|
165
142
|
.mockResolvedValueOnce('Did code review')
|
|
166
143
|
.mockResolvedValueOnce('Deploy to staging')
|
|
167
144
|
.mockResolvedValueOnce('');
|
|
168
145
|
await standupCommand({});
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
146
|
+
expect(mockClient.standups.submit).toHaveBeenCalledWith({
|
|
147
|
+
yesterday: 'Did code review',
|
|
148
|
+
today: 'Deploy to staging',
|
|
149
|
+
blockers: undefined,
|
|
150
|
+
});
|
|
172
151
|
});
|
|
173
152
|
it('re-prompts when required field is empty', async () => {
|
|
174
153
|
setupInteractiveAuth();
|
|
175
|
-
|
|
176
|
-
authenticatedFetch.mockResolvedValueOnce(submitResponse);
|
|
154
|
+
mockClient.standups.submit.mockResolvedValue({});
|
|
177
155
|
input
|
|
178
156
|
.mockResolvedValueOnce(' ') // empty yesterday -> re-prompt
|
|
179
157
|
.mockResolvedValueOnce('Fixed a bug') // valid yesterday
|
|
@@ -186,8 +164,7 @@ describe('standupCommand', () => {
|
|
|
186
164
|
});
|
|
187
165
|
it('opens editor when user types \\e', async () => {
|
|
188
166
|
setupInteractiveAuth();
|
|
189
|
-
|
|
190
|
-
authenticatedFetch.mockResolvedValueOnce(submitResponse);
|
|
167
|
+
mockClient.standups.submit.mockResolvedValue({});
|
|
191
168
|
input
|
|
192
169
|
.mockResolvedValueOnce('\\e') // trigger editor for yesterday
|
|
193
170
|
.mockResolvedValueOnce('Writing docs') // today
|
|
@@ -196,14 +173,13 @@ describe('standupCommand', () => {
|
|
|
196
173
|
editor.mockResolvedValueOnce('Detailed yesterday notes from editor');
|
|
197
174
|
await standupCommand({});
|
|
198
175
|
expect(editor).toHaveBeenCalledTimes(1);
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
176
|
+
expect(mockClient.standups.submit).toHaveBeenCalledWith(expect.objectContaining({
|
|
177
|
+
yesterday: 'Detailed yesterday notes from editor',
|
|
178
|
+
}));
|
|
202
179
|
});
|
|
203
180
|
it('falls back to inline input when editor fails', async () => {
|
|
204
181
|
setupInteractiveAuth();
|
|
205
|
-
|
|
206
|
-
authenticatedFetch.mockResolvedValueOnce(submitResponse);
|
|
182
|
+
mockClient.standups.submit.mockResolvedValue({});
|
|
207
183
|
input
|
|
208
184
|
.mockResolvedValueOnce('\\e') // trigger editor for yesterday
|
|
209
185
|
.mockResolvedValueOnce('Fallback input') // fallback after editor fails
|
|
@@ -218,12 +194,7 @@ describe('standupCommand', () => {
|
|
|
218
194
|
});
|
|
219
195
|
it('exits with error when API fails in interactive mode', async () => {
|
|
220
196
|
setupInteractiveAuth();
|
|
221
|
-
|
|
222
|
-
ok: false,
|
|
223
|
-
statusText: 'Unprocessable Entity',
|
|
224
|
-
json: vi.fn().mockResolvedValue({ error: 'Missing fields' }),
|
|
225
|
-
};
|
|
226
|
-
authenticatedFetch.mockResolvedValueOnce(submitResponse);
|
|
197
|
+
mockClient.standups.submit.mockRejectedValue(new Error('Missing fields'));
|
|
227
198
|
input
|
|
228
199
|
.mockResolvedValueOnce('Yesterday work')
|
|
229
200
|
.mockResolvedValueOnce('Today work')
|
|
@@ -234,12 +205,7 @@ describe('standupCommand', () => {
|
|
|
234
205
|
});
|
|
235
206
|
it('falls back to statusText when API error has no error body', async () => {
|
|
236
207
|
setupInteractiveAuth();
|
|
237
|
-
|
|
238
|
-
ok: false,
|
|
239
|
-
statusText: 'Bad Gateway',
|
|
240
|
-
json: vi.fn().mockResolvedValue({}),
|
|
241
|
-
};
|
|
242
|
-
authenticatedFetch.mockResolvedValueOnce(submitResponse);
|
|
208
|
+
mockClient.standups.submit.mockRejectedValue(new Error('Bad Gateway'));
|
|
243
209
|
input
|
|
244
210
|
.mockResolvedValueOnce('Yesterday')
|
|
245
211
|
.mockResolvedValueOnce('Today')
|
package/dist/utils/analytics.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from 'fs';
|
|
2
2
|
import { join, dirname } from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
|
+
import { createMeltClient } from '@meltstudio/meltctl-sdk';
|
|
4
5
|
import { getStoredAuth, API_BASE } from './auth.js';
|
|
5
6
|
import { getGitRepository, getGitBranch, getProjectName } from './git.js';
|
|
6
7
|
import { debugLog } from './debug.js';
|
|
@@ -27,15 +28,11 @@ export async function trackCommand(command, success, errorMessage) {
|
|
|
27
28
|
return;
|
|
28
29
|
}
|
|
29
30
|
const repo = getGitRepository();
|
|
31
|
+
const client = createMeltClient({ baseUrl: API_BASE, token: auth.token });
|
|
30
32
|
const controller = new AbortController();
|
|
31
33
|
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
headers: {
|
|
35
|
-
Authorization: `Bearer ${auth.token}`,
|
|
36
|
-
'Content-Type': 'application/json',
|
|
37
|
-
},
|
|
38
|
-
body: JSON.stringify({
|
|
34
|
+
try {
|
|
35
|
+
await client.events.submit({
|
|
39
36
|
command,
|
|
40
37
|
project: getProjectName(),
|
|
41
38
|
repository: repo?.slug ?? null,
|
|
@@ -43,20 +40,13 @@ export async function trackCommand(command, success, errorMessage) {
|
|
|
43
40
|
version: getVersion(),
|
|
44
41
|
success,
|
|
45
42
|
errorMessage: errorMessage?.slice(0, 500) ?? null,
|
|
46
|
-
})
|
|
47
|
-
signal: controller.signal,
|
|
48
|
-
}).catch(e => {
|
|
49
|
-
debugLog(`Analytics fetch failed: ${e instanceof Error ? e.message : e}`);
|
|
50
|
-
return null;
|
|
51
|
-
});
|
|
52
|
-
clearTimeout(timeout);
|
|
53
|
-
if (res && !res.ok) {
|
|
54
|
-
const body = await res.text().catch(() => '');
|
|
55
|
-
debugLog(`Analytics API error ${res.status}: ${body}`);
|
|
56
|
-
}
|
|
57
|
-
else if (res) {
|
|
43
|
+
});
|
|
58
44
|
debugLog(`Analytics event sent for "${command}"`);
|
|
59
45
|
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
debugLog(`Analytics error: ${e instanceof Error ? e.message : e}`);
|
|
48
|
+
}
|
|
49
|
+
clearTimeout(timeout);
|
|
60
50
|
}
|
|
61
51
|
catch (e) {
|
|
62
52
|
debugLog(`Analytics error: ${e instanceof Error ? e.message : e}`);
|
package/dist/utils/api.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
+
import { type MeltClient } from '@meltstudio/meltctl-sdk';
|
|
1
2
|
export declare function getToken(): Promise<string>;
|
|
2
|
-
export declare function
|
|
3
|
+
export declare function getClient(): Promise<MeltClient>;
|
package/dist/utils/api.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import { createMeltClient } from '@meltstudio/meltctl-sdk';
|
|
2
3
|
import { getStoredAuth, API_BASE } from './auth.js';
|
|
3
4
|
export async function getToken() {
|
|
4
5
|
const envToken = process.env['MELTCTL_TOKEN'];
|
|
@@ -16,16 +17,7 @@ export async function getToken() {
|
|
|
16
17
|
}
|
|
17
18
|
return auth.token;
|
|
18
19
|
}
|
|
19
|
-
export async function
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
headers: {
|
|
23
|
-
Authorization: `Bearer ${token}`,
|
|
24
|
-
...options.headers,
|
|
25
|
-
},
|
|
26
|
-
});
|
|
27
|
-
if (response.status === 401) {
|
|
28
|
-
throw new Error('Authentication failed. Run `npx @meltstudio/meltctl@latest login` or check your MELTCTL_TOKEN.');
|
|
29
|
-
}
|
|
30
|
-
return response;
|
|
20
|
+
export async function getClient() {
|
|
21
|
+
const token = await getToken();
|
|
22
|
+
return createMeltClient({ baseUrl: API_BASE, token });
|
|
31
23
|
}
|
package/dist/utils/api.test.js
CHANGED
|
@@ -3,8 +3,12 @@ vi.mock('./auth.js', () => ({
|
|
|
3
3
|
getStoredAuth: vi.fn(),
|
|
4
4
|
API_BASE: 'https://test-api.example.com',
|
|
5
5
|
}));
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
vi.mock('@meltstudio/meltctl-sdk', () => ({
|
|
7
|
+
createMeltClient: vi.fn().mockReturnValue({ mock: true }),
|
|
8
|
+
}));
|
|
9
|
+
import { getStoredAuth } from './auth.js';
|
|
10
|
+
import { createMeltClient } from '@meltstudio/meltctl-sdk';
|
|
11
|
+
import { getToken, getClient } from './api.js';
|
|
8
12
|
beforeEach(() => {
|
|
9
13
|
vi.clearAllMocks();
|
|
10
14
|
vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
@@ -47,50 +51,26 @@ describe('getToken', () => {
|
|
|
47
51
|
expect(console.error).toHaveBeenCalled();
|
|
48
52
|
});
|
|
49
53
|
});
|
|
50
|
-
describe('
|
|
51
|
-
it('
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
vi.unstubAllGlobals();
|
|
63
|
-
});
|
|
64
|
-
it('merges custom options and headers', async () => {
|
|
65
|
-
const fetchMock = vi.fn().mockResolvedValue({ status: 200, ok: true });
|
|
66
|
-
vi.stubGlobal('fetch', fetchMock);
|
|
67
|
-
await tokenFetch('tok', '/test', {
|
|
68
|
-
method: 'POST',
|
|
69
|
-
headers: { 'Content-Type': 'application/json' },
|
|
70
|
-
body: '{}',
|
|
54
|
+
describe('getClient', () => {
|
|
55
|
+
it('creates SDK client with token and base URL', async () => {
|
|
56
|
+
;
|
|
57
|
+
getStoredAuth.mockResolvedValue({
|
|
58
|
+
token: 'my-token',
|
|
59
|
+
email: 'dev@meltstudio.co',
|
|
60
|
+
expiresAt: '2099-12-31T00:00:00Z',
|
|
61
|
+
});
|
|
62
|
+
await getClient();
|
|
63
|
+
expect(createMeltClient).toHaveBeenCalledWith({
|
|
64
|
+
baseUrl: 'https://test-api.example.com',
|
|
65
|
+
token: 'my-token',
|
|
71
66
|
});
|
|
72
|
-
expect(fetchMock).toHaveBeenCalledWith(`${API_BASE}/test`, expect.objectContaining({
|
|
73
|
-
method: 'POST',
|
|
74
|
-
body: '{}',
|
|
75
|
-
headers: expect.objectContaining({
|
|
76
|
-
Authorization: 'Bearer tok',
|
|
77
|
-
'Content-Type': 'application/json',
|
|
78
|
-
}),
|
|
79
|
-
}));
|
|
80
|
-
vi.unstubAllGlobals();
|
|
81
|
-
});
|
|
82
|
-
it('throws when API returns 401', async () => {
|
|
83
|
-
const fetchMock = vi.fn().mockResolvedValue({ status: 401, ok: false });
|
|
84
|
-
vi.stubGlobal('fetch', fetchMock);
|
|
85
|
-
await expect(tokenFetch('bad-token', '/test')).rejects.toThrow('Authentication failed');
|
|
86
|
-
vi.unstubAllGlobals();
|
|
87
67
|
});
|
|
88
|
-
it('
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
68
|
+
it('uses env token when MELTCTL_TOKEN is set', async () => {
|
|
69
|
+
process.env['MELTCTL_TOKEN'] = 'env-token';
|
|
70
|
+
await getClient();
|
|
71
|
+
expect(createMeltClient).toHaveBeenCalledWith({
|
|
72
|
+
baseUrl: 'https://test-api.example.com',
|
|
73
|
+
token: 'env-token',
|
|
74
|
+
});
|
|
95
75
|
});
|
|
96
76
|
});
|
|
@@ -1,4 +1,2 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
}
|
|
4
|
-
export declare function fetchTemplates(): Promise<TemplateFiles>;
|
|
1
|
+
export type { TemplateFiles } from '@meltstudio/meltctl-sdk';
|
|
2
|
+
export declare function fetchTemplates(): Promise<Record<string, string>>;
|
package/dist/utils/templates.js
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getClient } from './api.js';
|
|
2
2
|
export async function fetchTemplates() {
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
throw new Error(`Failed to fetch templates: ${response.statusText}`);
|
|
6
|
-
}
|
|
7
|
-
const data = (await response.json());
|
|
8
|
-
return data.files;
|
|
3
|
+
const client = await getClient();
|
|
4
|
+
return client.templates.fetch();
|
|
9
5
|
}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
vi.
|
|
3
|
-
|
|
2
|
+
const mockClient = vi.hoisted(() => ({
|
|
3
|
+
templates: {
|
|
4
|
+
fetch: vi.fn(),
|
|
5
|
+
},
|
|
6
|
+
}));
|
|
7
|
+
vi.mock('./api.js', () => ({
|
|
8
|
+
getClient: vi.fn().mockResolvedValue(mockClient),
|
|
4
9
|
}));
|
|
5
|
-
import { authenticatedFetch } from './auth.js';
|
|
6
10
|
import { fetchTemplates } from './templates.js';
|
|
7
11
|
beforeEach(() => {
|
|
8
12
|
vi.clearAllMocks();
|
|
@@ -13,38 +17,22 @@ describe('fetchTemplates', () => {
|
|
|
13
17
|
'AGENTS.md': '# Agents content',
|
|
14
18
|
'.claude/skills/setup.md': '# Setup skill',
|
|
15
19
|
};
|
|
16
|
-
|
|
17
|
-
ok: true,
|
|
18
|
-
json: vi.fn().mockResolvedValue({ files: mockFiles }),
|
|
19
|
-
});
|
|
20
|
+
mockClient.templates.fetch.mockResolvedValue(mockFiles);
|
|
20
21
|
const result = await fetchTemplates();
|
|
21
|
-
expect(
|
|
22
|
+
expect(mockClient.templates.fetch).toHaveBeenCalled();
|
|
22
23
|
expect(result).toEqual(mockFiles);
|
|
23
24
|
});
|
|
24
25
|
it('throws error when API returns failure', async () => {
|
|
25
|
-
;
|
|
26
|
-
authenticatedFetch.mockResolvedValue({
|
|
27
|
-
ok: false,
|
|
28
|
-
statusText: 'Unauthorized',
|
|
29
|
-
});
|
|
26
|
+
mockClient.templates.fetch.mockRejectedValue(new Error('Failed to fetch templates: Unauthorized'));
|
|
30
27
|
await expect(fetchTemplates()).rejects.toThrow('Failed to fetch templates: Unauthorized');
|
|
31
28
|
});
|
|
32
29
|
it('throws error when API returns 500', async () => {
|
|
33
|
-
;
|
|
34
|
-
authenticatedFetch.mockResolvedValue({
|
|
35
|
-
ok: false,
|
|
36
|
-
statusText: 'Internal Server Error',
|
|
37
|
-
});
|
|
30
|
+
mockClient.templates.fetch.mockRejectedValue(new Error('Failed to fetch templates: Internal Server Error'));
|
|
38
31
|
await expect(fetchTemplates()).rejects.toThrow('Failed to fetch templates: Internal Server Error');
|
|
39
32
|
});
|
|
40
|
-
it('calls
|
|
41
|
-
;
|
|
42
|
-
authenticatedFetch.mockResolvedValue({
|
|
43
|
-
ok: true,
|
|
44
|
-
json: vi.fn().mockResolvedValue({ files: {} }),
|
|
45
|
-
});
|
|
33
|
+
it('calls client.templates.fetch', async () => {
|
|
34
|
+
mockClient.templates.fetch.mockResolvedValue({});
|
|
46
35
|
await fetchTemplates();
|
|
47
|
-
expect(
|
|
48
|
-
expect(authenticatedFetch).toHaveBeenCalledWith('/templates');
|
|
36
|
+
expect(mockClient.templates.fetch).toHaveBeenCalledTimes(1);
|
|
49
37
|
});
|
|
50
38
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meltstudio/meltctl",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.36.1",
|
|
4
4
|
"description": "AI-first development tools for teams - set up AGENTS.md, Claude Code, Cursor, and OpenCode standards",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"author": "Melt Studio",
|
|
48
48
|
"license": "UNLICENSED",
|
|
49
49
|
"dependencies": {
|
|
50
|
+
"@meltstudio/meltctl-sdk": "*",
|
|
50
51
|
"@commander-js/extra-typings": "^12.1.0",
|
|
51
52
|
"@inquirer/prompts": "^8.2.1",
|
|
52
53
|
"chalk": "^5.4.1",
|
|
@@ -60,7 +61,7 @@
|
|
|
60
61
|
"@typescript-eslint/eslint-plugin": "^8.19.0",
|
|
61
62
|
"@typescript-eslint/parser": "^8.19.0",
|
|
62
63
|
"eslint": "^9.17.0",
|
|
63
|
-
"eslint-config-prettier": "^
|
|
64
|
+
"eslint-config-prettier": "^10.1.8",
|
|
64
65
|
"eslint-plugin-prettier": "^5.2.1",
|
|
65
66
|
"prettier": "^3.4.2",
|
|
66
67
|
"tsx": "^4.19.5",
|