@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
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
2
|
vi.mock('../utils/auth.js', () => ({
|
|
3
3
|
isAuthenticated: vi.fn(),
|
|
4
|
-
authenticatedFetch: vi.fn(),
|
|
5
4
|
}));
|
|
6
|
-
|
|
5
|
+
const mockClient = vi.hoisted(() => ({
|
|
6
|
+
coins: {
|
|
7
|
+
getBalance: vi.fn(),
|
|
8
|
+
getLeaderboard: vi.fn(),
|
|
9
|
+
},
|
|
10
|
+
}));
|
|
11
|
+
vi.mock('../utils/api.js', () => ({
|
|
12
|
+
getClient: vi.fn().mockResolvedValue(mockClient),
|
|
13
|
+
}));
|
|
14
|
+
import { isAuthenticated } from '../utils/auth.js';
|
|
7
15
|
import { coinsCommand } from './coins.js';
|
|
8
16
|
beforeEach(() => {
|
|
9
17
|
vi.clearAllMocks();
|
|
@@ -24,23 +32,16 @@ describe('coinsCommand', () => {
|
|
|
24
32
|
it('displays coin balance on success', async () => {
|
|
25
33
|
;
|
|
26
34
|
isAuthenticated.mockResolvedValue(true);
|
|
27
|
-
|
|
28
|
-
ok: true,
|
|
29
|
-
json: vi.fn().mockResolvedValue({ coins: 5, period: '28d' }),
|
|
30
|
-
};
|
|
31
|
-
authenticatedFetch.mockResolvedValue(mockResponse);
|
|
35
|
+
mockClient.coins.getBalance.mockResolvedValue({ coins: 5, period: '28d' });
|
|
32
36
|
await coinsCommand({});
|
|
33
|
-
expect(
|
|
37
|
+
expect(mockClient.coins.getBalance).toHaveBeenCalled();
|
|
34
38
|
const logCalls = console.log.mock.calls.map((c) => String(c[0]));
|
|
35
39
|
expect(logCalls.some((msg) => msg.includes('5'))).toBe(true);
|
|
36
40
|
});
|
|
37
41
|
it('displays singular coin text for 1 coin', async () => {
|
|
38
42
|
;
|
|
39
43
|
isAuthenticated.mockResolvedValue(true);
|
|
40
|
-
|
|
41
|
-
ok: true,
|
|
42
|
-
json: vi.fn().mockResolvedValue({ coins: 1, period: '28d' }),
|
|
43
|
-
});
|
|
44
|
+
mockClient.coins.getBalance.mockResolvedValue({ coins: 1, period: '28d' });
|
|
44
45
|
await coinsCommand({});
|
|
45
46
|
const logCalls = console.log.mock.calls.map((c) => String(c[0]));
|
|
46
47
|
expect(logCalls.some((msg) => msg.includes('coin') && !msg.includes('coins'))).toBe(true);
|
|
@@ -48,10 +49,7 @@ describe('coinsCommand', () => {
|
|
|
48
49
|
it('displays plural coin text for multiple coins', async () => {
|
|
49
50
|
;
|
|
50
51
|
isAuthenticated.mockResolvedValue(true);
|
|
51
|
-
|
|
52
|
-
ok: true,
|
|
53
|
-
json: vi.fn().mockResolvedValue({ coins: 3, period: '28d' }),
|
|
54
|
-
});
|
|
52
|
+
mockClient.coins.getBalance.mockResolvedValue({ coins: 3, period: '28d' });
|
|
55
53
|
await coinsCommand({});
|
|
56
54
|
const logCalls = console.log.mock.calls.map((c) => String(c[0]));
|
|
57
55
|
expect(logCalls.some((msg) => msg.includes('coins'))).toBe(true);
|
|
@@ -59,12 +57,7 @@ describe('coinsCommand', () => {
|
|
|
59
57
|
it('exits with error when API returns failure', async () => {
|
|
60
58
|
;
|
|
61
59
|
isAuthenticated.mockResolvedValue(true);
|
|
62
|
-
|
|
63
|
-
ok: false,
|
|
64
|
-
statusText: 'Internal Server Error',
|
|
65
|
-
json: vi.fn().mockResolvedValue({ error: 'Something went wrong' }),
|
|
66
|
-
};
|
|
67
|
-
authenticatedFetch.mockResolvedValue(mockResponse);
|
|
60
|
+
mockClient.coins.getBalance.mockRejectedValue(new Error('Something went wrong'));
|
|
68
61
|
await expect(coinsCommand({})).rejects.toThrow('process.exit(1)');
|
|
69
62
|
const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
|
|
70
63
|
expect(errorCalls.some((msg) => msg.includes('Something went wrong'))).toBe(true);
|
|
@@ -84,12 +77,9 @@ describe('coinsCommand', () => {
|
|
|
84
77
|
{ name: 'Bob', coins: 7 },
|
|
85
78
|
{ name: 'Charlie', coins: 3 },
|
|
86
79
|
];
|
|
87
|
-
|
|
88
|
-
ok: true,
|
|
89
|
-
json: vi.fn().mockResolvedValue(entries),
|
|
90
|
-
});
|
|
80
|
+
mockClient.coins.getLeaderboard.mockResolvedValue(entries);
|
|
91
81
|
await coinsCommand({ leaderboard: true });
|
|
92
|
-
expect(
|
|
82
|
+
expect(mockClient.coins.getLeaderboard).toHaveBeenCalled();
|
|
93
83
|
const logCalls = console.log.mock.calls.map((c) => String(c[0]));
|
|
94
84
|
expect(logCalls.some((msg) => msg.includes('Leaderboard'))).toBe(true);
|
|
95
85
|
expect(logCalls.some((msg) => msg.includes('Alice'))).toBe(true);
|
|
@@ -98,10 +88,7 @@ describe('coinsCommand', () => {
|
|
|
98
88
|
it('shows message when no coins have been sent', async () => {
|
|
99
89
|
;
|
|
100
90
|
isAuthenticated.mockResolvedValue(true);
|
|
101
|
-
|
|
102
|
-
ok: true,
|
|
103
|
-
json: vi.fn().mockResolvedValue([]),
|
|
104
|
-
});
|
|
91
|
+
mockClient.coins.getLeaderboard.mockResolvedValue([]);
|
|
105
92
|
await coinsCommand({ leaderboard: true });
|
|
106
93
|
const logCalls = console.log.mock.calls.map((c) => String(c[0]));
|
|
107
94
|
expect(logCalls.some((msg) => msg.includes('No coins'))).toBe(true);
|
|
@@ -109,25 +96,18 @@ describe('coinsCommand', () => {
|
|
|
109
96
|
it('exits with error when leaderboard API fails', async () => {
|
|
110
97
|
;
|
|
111
98
|
isAuthenticated.mockResolvedValue(true);
|
|
112
|
-
|
|
113
|
-
ok: false,
|
|
114
|
-
statusText: 'Forbidden',
|
|
115
|
-
json: vi.fn().mockResolvedValue({ error: 'Access denied' }),
|
|
116
|
-
});
|
|
99
|
+
mockClient.coins.getLeaderboard.mockRejectedValue(new Error('Access denied'));
|
|
117
100
|
await expect(coinsCommand({ leaderboard: true })).rejects.toThrow('process.exit(1)');
|
|
118
101
|
const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
|
|
119
102
|
expect(errorCalls.some((msg) => msg.includes('Access denied'))).toBe(true);
|
|
120
103
|
});
|
|
121
|
-
it('calls
|
|
104
|
+
it('calls getLeaderboard not getBalance', async () => {
|
|
122
105
|
;
|
|
123
106
|
isAuthenticated.mockResolvedValue(true);
|
|
124
|
-
|
|
125
|
-
ok: true,
|
|
126
|
-
json: vi.fn().mockResolvedValue([{ name: 'Test', coins: 1 }]),
|
|
127
|
-
});
|
|
107
|
+
mockClient.coins.getLeaderboard.mockResolvedValue([{ name: 'Test', coins: 1 }]);
|
|
128
108
|
await coinsCommand({ leaderboard: true });
|
|
129
|
-
expect(
|
|
130
|
-
expect(
|
|
109
|
+
expect(mockClient.coins.getLeaderboard).toHaveBeenCalled();
|
|
110
|
+
expect(mockClient.coins.getBalance).not.toHaveBeenCalled();
|
|
131
111
|
});
|
|
132
112
|
});
|
|
133
113
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { input, select, editor } from '@inquirer/prompts';
|
|
3
|
-
import { isAuthenticated
|
|
3
|
+
import { isAuthenticated } from '../utils/auth.js';
|
|
4
|
+
import { getClient } from '../utils/api.js';
|
|
4
5
|
const EDITOR_HINT = chalk.dim('(type \\\\e to open your editor)');
|
|
5
6
|
async function promptDescription() {
|
|
6
7
|
const value = await input({
|
|
@@ -35,6 +36,7 @@ export async function feedbackCommand(options) {
|
|
|
35
36
|
console.error(chalk.red('Not authenticated. Run `npx @meltstudio/meltctl@latest login` first.'));
|
|
36
37
|
process.exit(1);
|
|
37
38
|
}
|
|
39
|
+
const client = await getClient();
|
|
38
40
|
let toUserId;
|
|
39
41
|
let coins;
|
|
40
42
|
let description;
|
|
@@ -49,13 +51,7 @@ export async function feedbackCommand(options) {
|
|
|
49
51
|
// Fetch recipients
|
|
50
52
|
let recipients;
|
|
51
53
|
try {
|
|
52
|
-
|
|
53
|
-
if (!res.ok) {
|
|
54
|
-
const body = (await res.json());
|
|
55
|
-
console.error(chalk.red(`Failed to load recipients: ${body.error ?? res.statusText}`));
|
|
56
|
-
process.exit(1);
|
|
57
|
-
}
|
|
58
|
-
recipients = (await res.json());
|
|
54
|
+
recipients = await client.feedback.getRecipients();
|
|
59
55
|
}
|
|
60
56
|
catch {
|
|
61
57
|
console.error(chalk.red('Failed to connect to the server.'));
|
|
@@ -83,17 +79,12 @@ export async function feedbackCommand(options) {
|
|
|
83
79
|
});
|
|
84
80
|
description = await promptDescription();
|
|
85
81
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
headers: { 'Content-Type': 'application/json' },
|
|
89
|
-
body: JSON.stringify({ toUserId, coins, description }),
|
|
90
|
-
});
|
|
91
|
-
if (res.ok) {
|
|
82
|
+
try {
|
|
83
|
+
await client.feedback.submit({ toUserId, coins, description });
|
|
92
84
|
console.log(chalk.green(`\n ✓ Feedback sent! You gave ${coins} coin${coins > 1 ? 's' : ''}.\n`));
|
|
93
85
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
console.error(chalk.red(`\nFailed to send feedback: ${body.error ?? res.statusText}`));
|
|
86
|
+
catch (error) {
|
|
87
|
+
console.error(chalk.red(`\nFailed to send feedback: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
97
88
|
process.exit(1);
|
|
98
89
|
}
|
|
99
90
|
}
|
|
@@ -1,14 +1,22 @@
|
|
|
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
|
+
feedback: {
|
|
7
|
+
submit: vi.fn(),
|
|
8
|
+
getRecipients: 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
|
select: vi.fn(),
|
|
9
17
|
editor: vi.fn(),
|
|
10
18
|
}));
|
|
11
|
-
import { isAuthenticated
|
|
19
|
+
import { isAuthenticated } from '../utils/auth.js';
|
|
12
20
|
import { input, select } from '@inquirer/prompts';
|
|
13
21
|
import { feedbackCommand } from './feedback.js';
|
|
14
22
|
beforeEach(() => {
|
|
@@ -29,24 +37,16 @@ describe('feedbackCommand', () => {
|
|
|
29
37
|
it('submits feedback with correct payload using option flags', async () => {
|
|
30
38
|
;
|
|
31
39
|
isAuthenticated.mockResolvedValue(true);
|
|
32
|
-
|
|
33
|
-
ok: true,
|
|
34
|
-
json: vi.fn().mockResolvedValue({}),
|
|
35
|
-
};
|
|
36
|
-
authenticatedFetch.mockResolvedValue(submitResponse);
|
|
40
|
+
mockClient.feedback.submit.mockResolvedValue({});
|
|
37
41
|
await feedbackCommand({
|
|
38
42
|
to: '42',
|
|
39
43
|
coins: '2',
|
|
40
44
|
description: 'Great job on the feature implementation!',
|
|
41
45
|
});
|
|
42
|
-
expect(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
toUserId: 42,
|
|
47
|
-
coins: 2,
|
|
48
|
-
description: 'Great job on the feature implementation!',
|
|
49
|
-
}),
|
|
46
|
+
expect(mockClient.feedback.submit).toHaveBeenCalledWith({
|
|
47
|
+
toUserId: 42,
|
|
48
|
+
coins: 2,
|
|
49
|
+
description: 'Great job on the feature implementation!',
|
|
50
50
|
});
|
|
51
51
|
const logCalls = console.log.mock.calls.map((c) => String(c[0]));
|
|
52
52
|
expect(logCalls.some((msg) => msg.includes('Feedback sent'))).toBe(true);
|
|
@@ -54,10 +54,7 @@ describe('feedbackCommand', () => {
|
|
|
54
54
|
it('displays singular coin text when sending 1 coin', async () => {
|
|
55
55
|
;
|
|
56
56
|
isAuthenticated.mockResolvedValue(true);
|
|
57
|
-
|
|
58
|
-
ok: true,
|
|
59
|
-
json: vi.fn().mockResolvedValue({}),
|
|
60
|
-
});
|
|
57
|
+
mockClient.feedback.submit.mockResolvedValue({});
|
|
61
58
|
await feedbackCommand({ to: '1', coins: '1', description: 'Nice work' });
|
|
62
59
|
const logCalls = console.log.mock.calls.map((c) => String(c[0]));
|
|
63
60
|
expect(logCalls.some((msg) => msg.includes('1 coin') && !msg.includes('1 coins'))).toBe(true);
|
|
@@ -65,10 +62,7 @@ describe('feedbackCommand', () => {
|
|
|
65
62
|
it('displays plural coin text when sending multiple coins', async () => {
|
|
66
63
|
;
|
|
67
64
|
isAuthenticated.mockResolvedValue(true);
|
|
68
|
-
|
|
69
|
-
ok: true,
|
|
70
|
-
json: vi.fn().mockResolvedValue({}),
|
|
71
|
-
});
|
|
65
|
+
mockClient.feedback.submit.mockResolvedValue({});
|
|
72
66
|
await feedbackCommand({ to: '1', coins: '3', description: 'Excellent work' });
|
|
73
67
|
const logCalls = console.log.mock.calls.map((c) => String(c[0]));
|
|
74
68
|
expect(logCalls.some((msg) => msg.includes('3 coins'))).toBe(true);
|
|
@@ -76,12 +70,7 @@ describe('feedbackCommand', () => {
|
|
|
76
70
|
it('exits with error when API returns failure', async () => {
|
|
77
71
|
;
|
|
78
72
|
isAuthenticated.mockResolvedValue(true);
|
|
79
|
-
|
|
80
|
-
ok: false,
|
|
81
|
-
statusText: 'Bad Request',
|
|
82
|
-
json: vi.fn().mockResolvedValue({ error: 'Insufficient coins' }),
|
|
83
|
-
};
|
|
84
|
-
authenticatedFetch.mockResolvedValue(submitResponse);
|
|
73
|
+
mockClient.feedback.submit.mockRejectedValue(new Error('Insufficient coins'));
|
|
85
74
|
await expect(feedbackCommand({ to: '42', coins: '2', description: 'Good work' })).rejects.toThrow('process.exit(1)');
|
|
86
75
|
const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
|
|
87
76
|
expect(errorCalls.some((msg) => msg.includes('Insufficient coins'))).toBe(true);
|
|
@@ -89,12 +78,7 @@ describe('feedbackCommand', () => {
|
|
|
89
78
|
it('exits with error when API returns failure without error body', async () => {
|
|
90
79
|
;
|
|
91
80
|
isAuthenticated.mockResolvedValue(true);
|
|
92
|
-
|
|
93
|
-
ok: false,
|
|
94
|
-
statusText: 'Internal Server Error',
|
|
95
|
-
json: vi.fn().mockResolvedValue({}),
|
|
96
|
-
};
|
|
97
|
-
authenticatedFetch.mockResolvedValue(submitResponse);
|
|
81
|
+
mockClient.feedback.submit.mockRejectedValue(new Error('Internal Server Error'));
|
|
98
82
|
await expect(feedbackCommand({ to: '42', coins: '2', description: 'Good work' })).rejects.toThrow('process.exit(1)');
|
|
99
83
|
const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
|
|
100
84
|
expect(errorCalls.some((msg) => msg.includes('Internal Server Error'))).toBe(true);
|
|
@@ -102,20 +86,13 @@ describe('feedbackCommand', () => {
|
|
|
102
86
|
it('sends correct numeric types in payload', async () => {
|
|
103
87
|
;
|
|
104
88
|
isAuthenticated.mockResolvedValue(true);
|
|
105
|
-
|
|
106
|
-
ok: true,
|
|
107
|
-
json: vi.fn().mockResolvedValue({}),
|
|
108
|
-
});
|
|
89
|
+
mockClient.feedback.submit.mockResolvedValue({});
|
|
109
90
|
await feedbackCommand({ to: '7', coins: '3', description: 'Awesome' });
|
|
110
|
-
|
|
111
|
-
const body = JSON.parse(call[1].body);
|
|
112
|
-
expect(body).toEqual({
|
|
91
|
+
expect(mockClient.feedback.submit).toHaveBeenCalledWith({
|
|
113
92
|
toUserId: 7,
|
|
114
93
|
coins: 3,
|
|
115
94
|
description: 'Awesome',
|
|
116
95
|
});
|
|
117
|
-
expect(typeof body.toUserId).toBe('number');
|
|
118
|
-
expect(typeof body.coins).toBe('number');
|
|
119
96
|
});
|
|
120
97
|
describe('interactive flow', () => {
|
|
121
98
|
const mockRecipients = [
|
|
@@ -128,73 +105,45 @@ describe('feedbackCommand', () => {
|
|
|
128
105
|
}
|
|
129
106
|
it('prompts for recipient, coins, and description in interactive mode', async () => {
|
|
130
107
|
setupInteractiveMocks();
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
json: vi.fn().mockResolvedValue(mockRecipients),
|
|
134
|
-
};
|
|
135
|
-
const submitResponse = {
|
|
136
|
-
ok: true,
|
|
137
|
-
json: vi.fn().mockResolvedValue({}),
|
|
138
|
-
};
|
|
139
|
-
authenticatedFetch
|
|
140
|
-
.mockResolvedValueOnce(recipientsResponse)
|
|
141
|
-
.mockResolvedValueOnce(submitResponse);
|
|
108
|
+
mockClient.feedback.getRecipients.mockResolvedValue(mockRecipients);
|
|
109
|
+
mockClient.feedback.submit.mockResolvedValue({});
|
|
142
110
|
select.mockResolvedValueOnce(1).mockResolvedValueOnce(2);
|
|
143
111
|
input.mockResolvedValueOnce('This is a great piece of feedback that is long enough to pass validation easily.');
|
|
144
112
|
await feedbackCommand({});
|
|
145
|
-
expect(
|
|
113
|
+
expect(mockClient.feedback.getRecipients).toHaveBeenCalled();
|
|
146
114
|
expect(select).toHaveBeenCalledTimes(2);
|
|
147
115
|
expect(input).toHaveBeenCalledTimes(1);
|
|
148
|
-
expect(
|
|
149
|
-
|
|
150
|
-
body: expect.stringContaining('"toUserId":1'),
|
|
116
|
+
expect(mockClient.feedback.submit).toHaveBeenCalledWith(expect.objectContaining({
|
|
117
|
+
toUserId: 1,
|
|
151
118
|
}));
|
|
152
119
|
});
|
|
153
120
|
it('shows no recipients message when list is empty', async () => {
|
|
154
121
|
setupInteractiveMocks();
|
|
155
|
-
|
|
156
|
-
ok: true,
|
|
157
|
-
json: vi.fn().mockResolvedValue([]),
|
|
158
|
-
};
|
|
159
|
-
authenticatedFetch.mockResolvedValueOnce(recipientsResponse);
|
|
122
|
+
mockClient.feedback.getRecipients.mockResolvedValue([]);
|
|
160
123
|
await feedbackCommand({});
|
|
161
124
|
const logCalls = console.log.mock.calls.map((c) => String(c[0]));
|
|
162
125
|
expect(logCalls.some((msg) => msg.includes('No recipients'))).toBe(true);
|
|
163
|
-
// Should not call
|
|
164
|
-
expect(
|
|
126
|
+
// Should not call submit
|
|
127
|
+
expect(mockClient.feedback.submit).not.toHaveBeenCalled();
|
|
165
128
|
});
|
|
166
129
|
it('exits with error when recipients API fails', async () => {
|
|
167
130
|
setupInteractiveMocks();
|
|
168
|
-
|
|
169
|
-
ok: false,
|
|
170
|
-
statusText: 'Forbidden',
|
|
171
|
-
json: vi.fn().mockResolvedValue({ error: 'Not allowed' }),
|
|
172
|
-
};
|
|
173
|
-
authenticatedFetch.mockResolvedValueOnce(recipientsResponse);
|
|
131
|
+
mockClient.feedback.getRecipients.mockRejectedValue(new Error('Not allowed'));
|
|
174
132
|
await expect(feedbackCommand({})).rejects.toThrow('process.exit(1)');
|
|
175
133
|
const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
|
|
176
|
-
expect(errorCalls.some((msg) => msg.includes('
|
|
134
|
+
expect(errorCalls.some((msg) => msg.includes('Failed to connect'))).toBe(true);
|
|
177
135
|
});
|
|
178
136
|
it('exits with error when recipients fetch throws network error', async () => {
|
|
179
137
|
setupInteractiveMocks();
|
|
180
|
-
|
|
138
|
+
mockClient.feedback.getRecipients.mockRejectedValue(new Error('Network error'));
|
|
181
139
|
await expect(feedbackCommand({})).rejects.toThrow('process.exit(1)');
|
|
182
140
|
const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
|
|
183
141
|
expect(errorCalls.some((msg) => msg.includes('Failed to connect'))).toBe(true);
|
|
184
142
|
});
|
|
185
143
|
it('re-prompts when description is too short', async () => {
|
|
186
144
|
setupInteractiveMocks();
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
json: vi.fn().mockResolvedValue(mockRecipients),
|
|
190
|
-
};
|
|
191
|
-
const submitResponse = {
|
|
192
|
-
ok: true,
|
|
193
|
-
json: vi.fn().mockResolvedValue({}),
|
|
194
|
-
};
|
|
195
|
-
authenticatedFetch
|
|
196
|
-
.mockResolvedValueOnce(recipientsResponse)
|
|
197
|
-
.mockResolvedValueOnce(submitResponse);
|
|
145
|
+
mockClient.feedback.getRecipients.mockResolvedValue(mockRecipients);
|
|
146
|
+
mockClient.feedback.submit.mockResolvedValue({});
|
|
198
147
|
select.mockResolvedValueOnce(2).mockResolvedValueOnce(3);
|
|
199
148
|
input
|
|
200
149
|
.mockResolvedValueOnce('Too short')
|
|
@@ -206,17 +155,8 @@ describe('feedbackCommand', () => {
|
|
|
206
155
|
});
|
|
207
156
|
it('re-prompts when description is too long', async () => {
|
|
208
157
|
setupInteractiveMocks();
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
json: vi.fn().mockResolvedValue(mockRecipients),
|
|
212
|
-
};
|
|
213
|
-
const submitResponse = {
|
|
214
|
-
ok: true,
|
|
215
|
-
json: vi.fn().mockResolvedValue({}),
|
|
216
|
-
};
|
|
217
|
-
authenticatedFetch
|
|
218
|
-
.mockResolvedValueOnce(recipientsResponse)
|
|
219
|
-
.mockResolvedValueOnce(submitResponse);
|
|
158
|
+
mockClient.feedback.getRecipients.mockResolvedValue(mockRecipients);
|
|
159
|
+
mockClient.feedback.submit.mockResolvedValue({});
|
|
220
160
|
select.mockResolvedValueOnce(1).mockResolvedValueOnce(1);
|
|
221
161
|
input
|
|
222
162
|
.mockResolvedValueOnce('x'.repeat(501))
|
|
@@ -228,15 +168,10 @@ describe('feedbackCommand', () => {
|
|
|
228
168
|
});
|
|
229
169
|
it('falls back to recipients error statusText when no error body', async () => {
|
|
230
170
|
setupInteractiveMocks();
|
|
231
|
-
|
|
232
|
-
ok: false,
|
|
233
|
-
statusText: 'Service Unavailable',
|
|
234
|
-
json: vi.fn().mockResolvedValue({}),
|
|
235
|
-
};
|
|
236
|
-
authenticatedFetch.mockResolvedValueOnce(recipientsResponse);
|
|
171
|
+
mockClient.feedback.getRecipients.mockRejectedValue(new Error('Service Unavailable'));
|
|
237
172
|
await expect(feedbackCommand({})).rejects.toThrow('process.exit(1)');
|
|
238
173
|
const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
|
|
239
|
-
expect(errorCalls.some((msg) => msg.includes('
|
|
174
|
+
expect(errorCalls.some((msg) => msg.includes('Failed to connect'))).toBe(true);
|
|
240
175
|
});
|
|
241
176
|
});
|
|
242
177
|
});
|
package/dist/commands/login.js
CHANGED
|
@@ -2,6 +2,7 @@ import chalk from 'chalk';
|
|
|
2
2
|
import http from 'http';
|
|
3
3
|
import { URL } from 'url';
|
|
4
4
|
import { exec } from 'child_process';
|
|
5
|
+
import { createMeltClient } from '@meltstudio/meltctl-sdk';
|
|
5
6
|
import { API_BASE, storeAuth } from '../utils/auth.js';
|
|
6
7
|
import { printBanner } from '../utils/banner.js';
|
|
7
8
|
const LOGIN_TIMEOUT_MS = 60_000;
|
|
@@ -70,26 +71,20 @@ export async function loginCommand() {
|
|
|
70
71
|
});
|
|
71
72
|
});
|
|
72
73
|
console.log(chalk.dim('Exchanging authorization code...'));
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
74
|
+
// Use SDK with empty token for the unauthenticated token exchange
|
|
75
|
+
const client = createMeltClient({ baseUrl: API_BASE, token: '' });
|
|
76
|
+
try {
|
|
77
|
+
const data = await client.auth.exchangeToken(authCode, redirectUri);
|
|
78
|
+
await storeAuth({
|
|
79
|
+
token: data.token,
|
|
80
|
+
email: data.email,
|
|
81
|
+
expiresAt: data.expiresAt,
|
|
82
|
+
});
|
|
83
|
+
console.log();
|
|
84
|
+
console.log(chalk.green(`Logged in as ${data.email}`));
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
console.error(chalk.red(`Authentication failed: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
85
88
|
process.exit(1);
|
|
86
89
|
}
|
|
87
|
-
const data = (await response.json());
|
|
88
|
-
await storeAuth({
|
|
89
|
-
token: data.token,
|
|
90
|
-
email: data.email,
|
|
91
|
-
expiresAt: data.expiresAt,
|
|
92
|
-
});
|
|
93
|
-
console.log();
|
|
94
|
-
console.log(chalk.green(`Logged in as ${data.email}`));
|
|
95
90
|
}
|
package/dist/commands/plan.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
import {
|
|
4
|
+
import { getClient } from '../utils/api.js';
|
|
5
5
|
import { getGitBranch, getGitCommit, getGitRepository, getProjectName, extractTicketId, findMdFiles, } from '../utils/git.js';
|
|
6
6
|
function extractFrontmatterStatus(content) {
|
|
7
7
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
@@ -29,7 +29,7 @@ async function autoDetectPlanFile() {
|
|
|
29
29
|
return mdFiles[0] ?? null;
|
|
30
30
|
}
|
|
31
31
|
export async function planSubmitCommand(file) {
|
|
32
|
-
const
|
|
32
|
+
const client = await getClient();
|
|
33
33
|
let filePath;
|
|
34
34
|
if (file) {
|
|
35
35
|
filePath = path.resolve(file);
|
|
@@ -55,38 +55,23 @@ export async function planSubmitCommand(file) {
|
|
|
55
55
|
const repo = getGitRepository();
|
|
56
56
|
const ticket = extractTicketId(branch) ?? extractTicketId(filename);
|
|
57
57
|
const status = extractFrontmatterStatus(content);
|
|
58
|
-
const payload = {
|
|
59
|
-
project,
|
|
60
|
-
repository: repo?.slug ?? null,
|
|
61
|
-
repositoryUrl: repo?.url ?? null,
|
|
62
|
-
branch,
|
|
63
|
-
commit,
|
|
64
|
-
content,
|
|
65
|
-
metadata: { filename },
|
|
66
|
-
};
|
|
67
|
-
if (ticket)
|
|
68
|
-
payload.ticket = ticket;
|
|
69
|
-
if (status)
|
|
70
|
-
payload.status = status;
|
|
71
58
|
try {
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
59
|
+
const result = await client.plans.submit({
|
|
60
|
+
project,
|
|
61
|
+
repository: repo?.slug ?? null,
|
|
62
|
+
repositoryUrl: repo?.url ?? null,
|
|
63
|
+
branch,
|
|
64
|
+
commit,
|
|
65
|
+
content,
|
|
66
|
+
ticket: ticket ?? null,
|
|
67
|
+
status: status ?? null,
|
|
68
|
+
metadata: { filename },
|
|
76
69
|
});
|
|
77
|
-
if (
|
|
78
|
-
|
|
79
|
-
if (body.created) {
|
|
80
|
-
console.log(chalk.green(`\n ✓ Plan submitted! ID: ${body.id}\n`));
|
|
81
|
-
}
|
|
82
|
-
else {
|
|
83
|
-
console.log(chalk.green(`\n ✓ Plan updated! ID: ${body.id}\n`));
|
|
84
|
-
}
|
|
70
|
+
if (result.created) {
|
|
71
|
+
console.log(chalk.green(`\n ✓ Plan submitted! ID: ${result.id}\n`));
|
|
85
72
|
}
|
|
86
73
|
else {
|
|
87
|
-
|
|
88
|
-
console.error(chalk.red(`\nFailed to submit plan: ${body.error ?? res.statusText}`));
|
|
89
|
-
process.exit(1);
|
|
74
|
+
console.log(chalk.green(`\n ✓ Plan updated! ID: ${result.id}\n`));
|
|
90
75
|
}
|
|
91
76
|
}
|
|
92
77
|
catch (error) {
|
|
@@ -95,28 +80,13 @@ export async function planSubmitCommand(file) {
|
|
|
95
80
|
}
|
|
96
81
|
}
|
|
97
82
|
export async function planListCommand(options) {
|
|
98
|
-
const
|
|
99
|
-
const params = new URLSearchParams();
|
|
100
|
-
if (options.repository)
|
|
101
|
-
params.set('repository', options.repository);
|
|
102
|
-
if (options.author)
|
|
103
|
-
params.set('author', options.author);
|
|
104
|
-
if (options.limit)
|
|
105
|
-
params.set('limit', options.limit);
|
|
106
|
-
const query = params.toString();
|
|
107
|
-
const urlPath = `/plans${query ? `?${query}` : ''}`;
|
|
83
|
+
const client = await getClient();
|
|
108
84
|
try {
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
if (!res.ok) {
|
|
115
|
-
const body = (await res.json());
|
|
116
|
-
console.error(chalk.red(`Failed to list plans: ${body.error ?? res.statusText}`));
|
|
117
|
-
process.exit(1);
|
|
118
|
-
}
|
|
119
|
-
const body = (await res.json());
|
|
85
|
+
const body = await client.plans.list({
|
|
86
|
+
repository: options.repository,
|
|
87
|
+
author: options.author,
|
|
88
|
+
limit: options.limit ? parseInt(options.limit, 10) : undefined,
|
|
89
|
+
});
|
|
120
90
|
if (body.plans.length === 0) {
|
|
121
91
|
console.log(chalk.dim('\n No plans found.\n'));
|
|
122
92
|
return;
|