@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.
- 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/commands/update.d.ts +2 -0
- package/dist/commands/update.js +74 -0
- package/dist/commands/update.test.d.ts +1 -0
- package/dist/commands/update.test.js +93 -0
- package/dist/index.js +7 -0
- 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/dist/utils/version-check.js +17 -15
- package/dist/utils/version-check.test.js +20 -2
- package/package.json +3 -2
|
@@ -5,9 +5,14 @@ vi.mock('fs-extra', () => ({
|
|
|
5
5
|
readFile: vi.fn(),
|
|
6
6
|
},
|
|
7
7
|
}));
|
|
8
|
+
const mockClient = vi.hoisted(() => ({
|
|
9
|
+
plans: {
|
|
10
|
+
submit: vi.fn(),
|
|
11
|
+
list: vi.fn(),
|
|
12
|
+
},
|
|
13
|
+
}));
|
|
8
14
|
vi.mock('../utils/api.js', () => ({
|
|
9
|
-
|
|
10
|
-
tokenFetch: vi.fn(),
|
|
15
|
+
getClient: vi.fn().mockResolvedValue(mockClient),
|
|
11
16
|
}));
|
|
12
17
|
vi.mock('../utils/git.js', () => ({
|
|
13
18
|
getGitBranch: vi.fn(),
|
|
@@ -18,7 +23,6 @@ vi.mock('../utils/git.js', () => ({
|
|
|
18
23
|
findMdFiles: vi.fn(),
|
|
19
24
|
}));
|
|
20
25
|
import fs from 'fs-extra';
|
|
21
|
-
import { getToken, tokenFetch } from '../utils/api.js';
|
|
22
26
|
import { getGitBranch, getGitCommit, getGitRepository, getProjectName, extractTicketId, findMdFiles, } from '../utils/git.js';
|
|
23
27
|
import { planSubmitCommand, planListCommand } from './plan.js';
|
|
24
28
|
beforeEach(() => {
|
|
@@ -32,7 +36,6 @@ beforeEach(() => {
|
|
|
32
36
|
describe('planSubmitCommand', () => {
|
|
33
37
|
function setupGitMocks() {
|
|
34
38
|
;
|
|
35
|
-
getToken.mockResolvedValue('test-token');
|
|
36
39
|
getGitBranch.mockReturnValue('feature/PROJ-123-add-feature');
|
|
37
40
|
getGitCommit.mockReturnValue('def5678');
|
|
38
41
|
getGitRepository.mockReturnValue({
|
|
@@ -46,15 +49,14 @@ describe('planSubmitCommand', () => {
|
|
|
46
49
|
setupGitMocks();
|
|
47
50
|
fs.pathExists.mockResolvedValue(true);
|
|
48
51
|
fs.readFile.mockResolvedValue('# Plan\nSome plan content.');
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
mockClient.plans.submit.mockResolvedValue({
|
|
53
|
+
id: 'plan-100',
|
|
54
|
+
createdAt: '2026-03-26T00:00:00Z',
|
|
55
|
+
updatedAt: '2026-03-26T00:00:00Z',
|
|
56
|
+
created: true,
|
|
57
|
+
});
|
|
54
58
|
await planSubmitCommand('PROJ-123-plan.md');
|
|
55
|
-
|
|
56
|
-
const body = JSON.parse(call[2].body);
|
|
57
|
-
expect(body).toEqual({
|
|
59
|
+
expect(mockClient.plans.submit).toHaveBeenCalledWith({
|
|
58
60
|
project: 'test-project',
|
|
59
61
|
repository: 'Org/Repo',
|
|
60
62
|
repositoryUrl: 'https://github.com/Org/Repo.git',
|
|
@@ -63,17 +65,19 @@ describe('planSubmitCommand', () => {
|
|
|
63
65
|
content: '# Plan\nSome plan content.',
|
|
64
66
|
metadata: { filename: 'PROJ-123-plan.md' },
|
|
65
67
|
ticket: 'PROJ-123',
|
|
68
|
+
status: null,
|
|
66
69
|
});
|
|
67
70
|
});
|
|
68
71
|
it('logs "Plan submitted" when API returns created: true', async () => {
|
|
69
72
|
setupGitMocks();
|
|
70
73
|
fs.pathExists.mockResolvedValue(true);
|
|
71
74
|
fs.readFile.mockResolvedValue('plan content');
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
mockClient.plans.submit.mockResolvedValue({
|
|
76
|
+
id: 'plan-new',
|
|
77
|
+
createdAt: '2026-03-26T00:00:00Z',
|
|
78
|
+
updatedAt: '2026-03-26T00:00:00Z',
|
|
79
|
+
created: true,
|
|
80
|
+
});
|
|
77
81
|
await planSubmitCommand('plan.md');
|
|
78
82
|
const logCalls = console.log.mock.calls.map((c) => c[0]);
|
|
79
83
|
expect(logCalls.some((msg) => typeof msg === 'string' && msg.includes('Plan submitted'))).toBe(true);
|
|
@@ -82,11 +86,12 @@ describe('planSubmitCommand', () => {
|
|
|
82
86
|
setupGitMocks();
|
|
83
87
|
fs.pathExists.mockResolvedValue(true);
|
|
84
88
|
fs.readFile.mockResolvedValue('updated plan content');
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
89
|
+
mockClient.plans.submit.mockResolvedValue({
|
|
90
|
+
id: 'plan-upd',
|
|
91
|
+
createdAt: '2026-03-26T00:00:00Z',
|
|
92
|
+
updatedAt: '2026-03-26T00:00:00Z',
|
|
93
|
+
created: false,
|
|
94
|
+
});
|
|
90
95
|
await planSubmitCommand('plan.md');
|
|
91
96
|
const logCalls = console.log.mock.calls.map((c) => c[0]);
|
|
92
97
|
expect(logCalls.some((msg) => typeof msg === 'string' && msg.includes('Plan updated'))).toBe(true);
|
|
@@ -101,12 +106,7 @@ describe('planSubmitCommand', () => {
|
|
|
101
106
|
setupGitMocks();
|
|
102
107
|
fs.pathExists.mockResolvedValue(true);
|
|
103
108
|
fs.readFile.mockResolvedValue('content');
|
|
104
|
-
|
|
105
|
-
ok: false,
|
|
106
|
-
statusText: 'Bad Request',
|
|
107
|
-
json: vi.fn().mockResolvedValue({ error: 'Invalid content' }),
|
|
108
|
-
};
|
|
109
|
-
tokenFetch.mockResolvedValue(mockResponse);
|
|
109
|
+
mockClient.plans.submit.mockRejectedValue(new Error('Invalid content'));
|
|
110
110
|
await expect(planSubmitCommand('plan.md')).rejects.toThrow('process.exit(1)');
|
|
111
111
|
});
|
|
112
112
|
it('auto-detects plan file from .plans/ directory when no file provided', async () => {
|
|
@@ -115,19 +115,19 @@ describe('planSubmitCommand', () => {
|
|
|
115
115
|
extractTicketId.mockReturnValue('PROJ-123');
|
|
116
116
|
fs.pathExists.mockResolvedValue(true);
|
|
117
117
|
fs.readFile.mockResolvedValue('auto-detected plan');
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
118
|
+
mockClient.plans.submit.mockResolvedValue({
|
|
119
|
+
id: 'plan-auto',
|
|
120
|
+
createdAt: '2026-03-26T00:00:00Z',
|
|
121
|
+
updatedAt: '2026-03-26T00:00:00Z',
|
|
122
|
+
created: true,
|
|
123
|
+
});
|
|
123
124
|
await planSubmitCommand();
|
|
124
|
-
expect(
|
|
125
|
-
|
|
125
|
+
expect(mockClient.plans.submit).toHaveBeenCalledWith(expect.objectContaining({
|
|
126
|
+
content: 'auto-detected plan',
|
|
126
127
|
}));
|
|
127
128
|
});
|
|
128
129
|
it('exits with error when no file provided and none auto-detected', async () => {
|
|
129
130
|
;
|
|
130
|
-
getToken.mockResolvedValue('test-token');
|
|
131
131
|
findMdFiles.mockResolvedValue([]);
|
|
132
132
|
getGitBranch.mockReturnValue('main');
|
|
133
133
|
extractTicketId.mockReturnValue(null);
|
|
@@ -136,7 +136,6 @@ describe('planSubmitCommand', () => {
|
|
|
136
136
|
});
|
|
137
137
|
it('does not include ticket in payload when extractTicketId returns null', async () => {
|
|
138
138
|
;
|
|
139
|
-
getToken.mockResolvedValue('test-token');
|
|
140
139
|
getGitBranch.mockReturnValue('main');
|
|
141
140
|
getGitCommit.mockReturnValue('abc1234');
|
|
142
141
|
getGitRepository.mockReturnValue({
|
|
@@ -147,137 +146,101 @@ describe('planSubmitCommand', () => {
|
|
|
147
146
|
extractTicketId.mockReturnValue(null);
|
|
148
147
|
fs.pathExists.mockResolvedValue(true);
|
|
149
148
|
fs.readFile.mockResolvedValue('plan without ticket');
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
149
|
+
mockClient.plans.submit.mockResolvedValue({
|
|
150
|
+
id: 'plan-no-ticket',
|
|
151
|
+
createdAt: '2026-03-26T00:00:00Z',
|
|
152
|
+
updatedAt: '2026-03-26T00:00:00Z',
|
|
153
|
+
created: true,
|
|
154
|
+
});
|
|
155
155
|
await planSubmitCommand('plan.md');
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
156
|
+
expect(mockClient.plans.submit).toHaveBeenCalledWith(expect.objectContaining({
|
|
157
|
+
ticket: null,
|
|
158
|
+
}));
|
|
159
159
|
});
|
|
160
160
|
it('includes status from frontmatter when present', async () => {
|
|
161
161
|
setupGitMocks();
|
|
162
162
|
fs.pathExists.mockResolvedValue(true);
|
|
163
163
|
fs.readFile.mockResolvedValue('---\nstatus: approved\n---\n# Plan');
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
164
|
+
mockClient.plans.submit.mockResolvedValue({
|
|
165
|
+
id: 'plan-status',
|
|
166
|
+
createdAt: '2026-03-26T00:00:00Z',
|
|
167
|
+
updatedAt: '2026-03-26T00:00:00Z',
|
|
168
|
+
created: true,
|
|
169
|
+
});
|
|
169
170
|
await planSubmitCommand('plan.md');
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
171
|
+
expect(mockClient.plans.submit).toHaveBeenCalledWith(expect.objectContaining({
|
|
172
|
+
status: 'approved',
|
|
173
|
+
}));
|
|
173
174
|
});
|
|
174
175
|
it('does not include status when no frontmatter present', async () => {
|
|
175
176
|
setupGitMocks();
|
|
176
177
|
fs.pathExists.mockResolvedValue(true);
|
|
177
178
|
fs.readFile.mockResolvedValue('# Plan\nNo frontmatter here.');
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
179
|
+
mockClient.plans.submit.mockResolvedValue({
|
|
180
|
+
id: 'plan-no-status',
|
|
181
|
+
createdAt: '2026-03-26T00:00:00Z',
|
|
182
|
+
updatedAt: '2026-03-26T00:00:00Z',
|
|
183
|
+
created: true,
|
|
184
|
+
});
|
|
183
185
|
await planSubmitCommand('plan.md');
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
186
|
+
expect(mockClient.plans.submit).toHaveBeenCalledWith(expect.objectContaining({
|
|
187
|
+
status: null,
|
|
188
|
+
}));
|
|
187
189
|
});
|
|
188
190
|
});
|
|
189
191
|
describe('planListCommand', () => {
|
|
190
192
|
it('calls API with correct query params', async () => {
|
|
191
|
-
;
|
|
192
|
-
getToken.mockResolvedValue('test-token');
|
|
193
|
-
const mockResponse = {
|
|
194
|
-
ok: true,
|
|
195
|
-
status: 200,
|
|
196
|
-
json: vi.fn().mockResolvedValue({ plans: [], count: 0 }),
|
|
197
|
-
};
|
|
198
|
-
tokenFetch.mockResolvedValue(mockResponse);
|
|
193
|
+
mockClient.plans.list.mockResolvedValue({ plans: [], count: 0 });
|
|
199
194
|
await planListCommand({ repository: 'Org/Repo', author: 'dev@meltstudio.co', limit: '5' });
|
|
200
|
-
expect(
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
195
|
+
expect(mockClient.plans.list).toHaveBeenCalledWith({
|
|
196
|
+
repository: 'Org/Repo',
|
|
197
|
+
author: 'dev@meltstudio.co',
|
|
198
|
+
limit: 5,
|
|
199
|
+
});
|
|
205
200
|
});
|
|
206
201
|
it('exits with error on 403 response', async () => {
|
|
207
|
-
;
|
|
208
|
-
getToken.mockResolvedValue('test-token');
|
|
209
|
-
const mockResponse = {
|
|
210
|
-
ok: false,
|
|
211
|
-
status: 403,
|
|
212
|
-
statusText: 'Forbidden',
|
|
213
|
-
};
|
|
214
|
-
tokenFetch.mockResolvedValue(mockResponse);
|
|
202
|
+
mockClient.plans.list.mockRejectedValue(new Error('Access denied. Only Team Managers can list plans.'));
|
|
215
203
|
await expect(planListCommand({})).rejects.toThrow('process.exit(1)');
|
|
216
204
|
expect(console.error).toHaveBeenCalled();
|
|
217
205
|
});
|
|
218
206
|
it('displays plan list when plans exist', async () => {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
updatedAt: '2026-03-25T12:00:00Z',
|
|
237
|
-
},
|
|
238
|
-
],
|
|
239
|
-
count: 1,
|
|
240
|
-
}),
|
|
241
|
-
};
|
|
242
|
-
tokenFetch.mockResolvedValue(mockResponse);
|
|
207
|
+
mockClient.plans.list.mockResolvedValue({
|
|
208
|
+
plans: [
|
|
209
|
+
{
|
|
210
|
+
id: '1',
|
|
211
|
+
project: 'my-project',
|
|
212
|
+
repository: 'Org/Repo',
|
|
213
|
+
author: 'dev@meltstudio.co',
|
|
214
|
+
branch: 'feature/PROJ-123',
|
|
215
|
+
commit: 'abc1234',
|
|
216
|
+
ticket: 'PROJ-123',
|
|
217
|
+
status: 'submitted',
|
|
218
|
+
createdAt: '2026-03-25T10:00:00Z',
|
|
219
|
+
updatedAt: '2026-03-25T12:00:00Z',
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
count: 1,
|
|
223
|
+
});
|
|
243
224
|
await planListCommand({});
|
|
244
225
|
expect(console.log).toHaveBeenCalled();
|
|
245
226
|
});
|
|
246
227
|
it('shows "No plans found" when list is empty', async () => {
|
|
247
|
-
;
|
|
248
|
-
getToken.mockResolvedValue('test-token');
|
|
249
|
-
const mockResponse = {
|
|
250
|
-
ok: true,
|
|
251
|
-
status: 200,
|
|
252
|
-
json: vi.fn().mockResolvedValue({ plans: [], count: 0 }),
|
|
253
|
-
};
|
|
254
|
-
tokenFetch.mockResolvedValue(mockResponse);
|
|
228
|
+
mockClient.plans.list.mockResolvedValue({ plans: [], count: 0 });
|
|
255
229
|
await planListCommand({});
|
|
256
230
|
const logCalls = console.log.mock.calls.map((c) => c[0]);
|
|
257
231
|
expect(logCalls.some((msg) => typeof msg === 'string' && msg.includes('No plans found'))).toBe(true);
|
|
258
232
|
});
|
|
259
233
|
it('calls API without query params when no options provided', async () => {
|
|
260
|
-
;
|
|
261
|
-
getToken.mockResolvedValue('test-token');
|
|
262
|
-
const mockResponse = {
|
|
263
|
-
ok: true,
|
|
264
|
-
status: 200,
|
|
265
|
-
json: vi.fn().mockResolvedValue({ plans: [], count: 0 }),
|
|
266
|
-
};
|
|
267
|
-
tokenFetch.mockResolvedValue(mockResponse);
|
|
234
|
+
mockClient.plans.list.mockResolvedValue({ plans: [], count: 0 });
|
|
268
235
|
await planListCommand({});
|
|
269
|
-
expect(
|
|
236
|
+
expect(mockClient.plans.list).toHaveBeenCalledWith({
|
|
237
|
+
repository: undefined,
|
|
238
|
+
author: undefined,
|
|
239
|
+
limit: undefined,
|
|
240
|
+
});
|
|
270
241
|
});
|
|
271
242
|
it('exits with error on non-403 failure response', async () => {
|
|
272
|
-
;
|
|
273
|
-
getToken.mockResolvedValue('test-token');
|
|
274
|
-
const mockResponse = {
|
|
275
|
-
ok: false,
|
|
276
|
-
status: 500,
|
|
277
|
-
statusText: 'Internal Server Error',
|
|
278
|
-
json: vi.fn().mockResolvedValue({ error: 'Server error' }),
|
|
279
|
-
};
|
|
280
|
-
tokenFetch.mockResolvedValue(mockResponse);
|
|
243
|
+
mockClient.plans.list.mockRejectedValue(new Error('Server error'));
|
|
281
244
|
await expect(planListCommand({})).rejects.toThrow('process.exit(1)');
|
|
282
245
|
});
|
|
283
246
|
});
|
package/dist/commands/standup.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { input, 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 promptField(message, required) {
|
|
6
7
|
const value = await input({ message: `${message} ${EDITOR_HINT}` });
|
|
@@ -24,11 +25,11 @@ export async function standupCommand(options) {
|
|
|
24
25
|
console.error(chalk.red('Not authenticated. Run `npx @meltstudio/meltctl@latest login` first.'));
|
|
25
26
|
process.exit(1);
|
|
26
27
|
}
|
|
28
|
+
const client = await getClient();
|
|
27
29
|
// Check if already submitted today
|
|
28
30
|
try {
|
|
29
|
-
const
|
|
30
|
-
if (
|
|
31
|
-
const existing = (await statusRes.json());
|
|
31
|
+
const existing = await client.standups.getStatus();
|
|
32
|
+
if (existing) {
|
|
32
33
|
console.log(chalk.yellow('\nYou already submitted a standup today:\n'));
|
|
33
34
|
console.log(chalk.bold('Yesterday:'), existing.yesterday);
|
|
34
35
|
console.log(chalk.bold('Today:'), existing.today);
|
|
@@ -58,21 +59,16 @@ export async function standupCommand(options) {
|
|
|
58
59
|
today = await promptField('What are you going to work on today?', true);
|
|
59
60
|
blockers = await promptField('Any blockers? (leave empty if none)', false);
|
|
60
61
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
headers: { 'Content-Type': 'application/json' },
|
|
64
|
-
body: JSON.stringify({
|
|
62
|
+
try {
|
|
63
|
+
await client.standups.submit({
|
|
65
64
|
yesterday,
|
|
66
65
|
today,
|
|
67
66
|
blockers: blockers || undefined,
|
|
68
|
-
})
|
|
69
|
-
});
|
|
70
|
-
if (res.ok) {
|
|
67
|
+
});
|
|
71
68
|
console.log(chalk.green('\n ✓ Standup submitted!\n'));
|
|
72
69
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
console.error(chalk.red(`\nFailed to submit standup: ${body.error ?? res.statusText}`));
|
|
70
|
+
catch (error) {
|
|
71
|
+
console.error(chalk.red(`\nFailed to submit standup: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
76
72
|
process.exit(1);
|
|
77
73
|
}
|
|
78
74
|
}
|