@meltstudio/meltctl 4.27.0 → 4.28.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 +30 -5
- package/dist/commands/audit.test.d.ts +1 -0
- package/dist/commands/audit.test.js +250 -0
- package/dist/commands/coins.test.d.ts +1 -0
- package/dist/commands/coins.test.js +133 -0
- package/dist/commands/feedback.test.d.ts +1 -0
- package/dist/commands/feedback.test.js +242 -0
- package/dist/commands/init.js +21 -4
- package/dist/commands/init.test.d.ts +1 -0
- package/dist/commands/init.test.js +476 -0
- package/dist/commands/login.test.d.ts +1 -0
- package/dist/commands/login.test.js +174 -0
- package/dist/commands/logout.test.d.ts +1 -0
- package/dist/commands/logout.test.js +59 -0
- package/dist/commands/plan.test.d.ts +1 -0
- package/dist/commands/plan.test.js +283 -0
- package/dist/commands/standup.test.d.ts +1 -0
- package/dist/commands/standup.test.js +252 -0
- package/dist/commands/templates.test.d.ts +1 -0
- package/dist/commands/templates.test.js +89 -0
- package/dist/commands/version.test.d.ts +1 -0
- package/dist/commands/version.test.js +86 -0
- package/dist/index.js +1 -1
- package/dist/utils/api.test.d.ts +1 -0
- package/dist/utils/api.test.js +96 -0
- package/dist/utils/auth.test.d.ts +1 -0
- package/dist/utils/auth.test.js +165 -0
- package/dist/utils/banner.test.d.ts +1 -0
- package/dist/utils/banner.test.js +34 -0
- package/dist/utils/git.test.d.ts +1 -0
- package/dist/utils/git.test.js +184 -0
- package/dist/utils/package-manager.test.d.ts +1 -0
- package/dist/utils/package-manager.test.js +76 -0
- package/dist/utils/templates.test.d.ts +1 -0
- package/dist/utils/templates.test.js +50 -0
- package/dist/utils/version-check.test.d.ts +1 -0
- package/dist/utils/version-check.test.js +135 -0
- package/package.json +2 -1
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
vi.mock('../utils/auth.js', () => ({
|
|
3
|
+
isAuthenticated: vi.fn(),
|
|
4
|
+
authenticatedFetch: vi.fn(),
|
|
5
|
+
}));
|
|
6
|
+
vi.mock('@inquirer/prompts', () => ({
|
|
7
|
+
input: vi.fn(),
|
|
8
|
+
select: vi.fn(),
|
|
9
|
+
editor: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
import { isAuthenticated, authenticatedFetch } from '../utils/auth.js';
|
|
12
|
+
import { input, select } from '@inquirer/prompts';
|
|
13
|
+
import { feedbackCommand } from './feedback.js';
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
vi.clearAllMocks();
|
|
16
|
+
vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
17
|
+
vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
18
|
+
vi.spyOn(process, 'exit').mockImplementation((code) => {
|
|
19
|
+
throw new Error(`process.exit(${code})`);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
describe('feedbackCommand', () => {
|
|
23
|
+
it('exits with error when not authenticated', async () => {
|
|
24
|
+
;
|
|
25
|
+
isAuthenticated.mockResolvedValue(false);
|
|
26
|
+
await expect(feedbackCommand({})).rejects.toThrow('process.exit(1)');
|
|
27
|
+
expect(console.error).toHaveBeenCalled();
|
|
28
|
+
});
|
|
29
|
+
it('submits feedback with correct payload using option flags', async () => {
|
|
30
|
+
;
|
|
31
|
+
isAuthenticated.mockResolvedValue(true);
|
|
32
|
+
const submitResponse = {
|
|
33
|
+
ok: true,
|
|
34
|
+
json: vi.fn().mockResolvedValue({}),
|
|
35
|
+
};
|
|
36
|
+
authenticatedFetch.mockResolvedValue(submitResponse);
|
|
37
|
+
await feedbackCommand({
|
|
38
|
+
to: '42',
|
|
39
|
+
coins: '2',
|
|
40
|
+
description: 'Great job on the feature implementation!',
|
|
41
|
+
});
|
|
42
|
+
expect(authenticatedFetch).toHaveBeenCalledWith('/feedback', {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers: { 'Content-Type': 'application/json' },
|
|
45
|
+
body: JSON.stringify({
|
|
46
|
+
toUserId: 42,
|
|
47
|
+
coins: 2,
|
|
48
|
+
description: 'Great job on the feature implementation!',
|
|
49
|
+
}),
|
|
50
|
+
});
|
|
51
|
+
const logCalls = console.log.mock.calls.map((c) => String(c[0]));
|
|
52
|
+
expect(logCalls.some((msg) => msg.includes('Feedback sent'))).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
it('displays singular coin text when sending 1 coin', async () => {
|
|
55
|
+
;
|
|
56
|
+
isAuthenticated.mockResolvedValue(true);
|
|
57
|
+
authenticatedFetch.mockResolvedValue({
|
|
58
|
+
ok: true,
|
|
59
|
+
json: vi.fn().mockResolvedValue({}),
|
|
60
|
+
});
|
|
61
|
+
await feedbackCommand({ to: '1', coins: '1', description: 'Nice work' });
|
|
62
|
+
const logCalls = console.log.mock.calls.map((c) => String(c[0]));
|
|
63
|
+
expect(logCalls.some((msg) => msg.includes('1 coin') && !msg.includes('1 coins'))).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
it('displays plural coin text when sending multiple coins', async () => {
|
|
66
|
+
;
|
|
67
|
+
isAuthenticated.mockResolvedValue(true);
|
|
68
|
+
authenticatedFetch.mockResolvedValue({
|
|
69
|
+
ok: true,
|
|
70
|
+
json: vi.fn().mockResolvedValue({}),
|
|
71
|
+
});
|
|
72
|
+
await feedbackCommand({ to: '1', coins: '3', description: 'Excellent work' });
|
|
73
|
+
const logCalls = console.log.mock.calls.map((c) => String(c[0]));
|
|
74
|
+
expect(logCalls.some((msg) => msg.includes('3 coins'))).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
it('exits with error when API returns failure', async () => {
|
|
77
|
+
;
|
|
78
|
+
isAuthenticated.mockResolvedValue(true);
|
|
79
|
+
const submitResponse = {
|
|
80
|
+
ok: false,
|
|
81
|
+
statusText: 'Bad Request',
|
|
82
|
+
json: vi.fn().mockResolvedValue({ error: 'Insufficient coins' }),
|
|
83
|
+
};
|
|
84
|
+
authenticatedFetch.mockResolvedValue(submitResponse);
|
|
85
|
+
await expect(feedbackCommand({ to: '42', coins: '2', description: 'Good work' })).rejects.toThrow('process.exit(1)');
|
|
86
|
+
const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
|
|
87
|
+
expect(errorCalls.some((msg) => msg.includes('Insufficient coins'))).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
it('exits with error when API returns failure without error body', async () => {
|
|
90
|
+
;
|
|
91
|
+
isAuthenticated.mockResolvedValue(true);
|
|
92
|
+
const submitResponse = {
|
|
93
|
+
ok: false,
|
|
94
|
+
statusText: 'Internal Server Error',
|
|
95
|
+
json: vi.fn().mockResolvedValue({}),
|
|
96
|
+
};
|
|
97
|
+
authenticatedFetch.mockResolvedValue(submitResponse);
|
|
98
|
+
await expect(feedbackCommand({ to: '42', coins: '2', description: 'Good work' })).rejects.toThrow('process.exit(1)');
|
|
99
|
+
const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
|
|
100
|
+
expect(errorCalls.some((msg) => msg.includes('Internal Server Error'))).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
it('sends correct numeric types in payload', async () => {
|
|
103
|
+
;
|
|
104
|
+
isAuthenticated.mockResolvedValue(true);
|
|
105
|
+
authenticatedFetch.mockResolvedValue({
|
|
106
|
+
ok: true,
|
|
107
|
+
json: vi.fn().mockResolvedValue({}),
|
|
108
|
+
});
|
|
109
|
+
await feedbackCommand({ to: '7', coins: '3', description: 'Awesome' });
|
|
110
|
+
const call = authenticatedFetch.mock.calls[0];
|
|
111
|
+
const body = JSON.parse(call[1].body);
|
|
112
|
+
expect(body).toEqual({
|
|
113
|
+
toUserId: 7,
|
|
114
|
+
coins: 3,
|
|
115
|
+
description: 'Awesome',
|
|
116
|
+
});
|
|
117
|
+
expect(typeof body.toUserId).toBe('number');
|
|
118
|
+
expect(typeof body.coins).toBe('number');
|
|
119
|
+
});
|
|
120
|
+
describe('interactive flow', () => {
|
|
121
|
+
const mockRecipients = [
|
|
122
|
+
{ id: 1, firstName: 'Alice', lastName: 'Smith', email: 'alice@meltstudio.co' },
|
|
123
|
+
{ id: 2, firstName: 'Bob', lastName: 'Jones', email: 'bob@meltstudio.co' },
|
|
124
|
+
];
|
|
125
|
+
function setupInteractiveMocks() {
|
|
126
|
+
;
|
|
127
|
+
isAuthenticated.mockResolvedValue(true);
|
|
128
|
+
}
|
|
129
|
+
it('prompts for recipient, coins, and description in interactive mode', async () => {
|
|
130
|
+
setupInteractiveMocks();
|
|
131
|
+
const recipientsResponse = {
|
|
132
|
+
ok: true,
|
|
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);
|
|
142
|
+
select.mockResolvedValueOnce(1).mockResolvedValueOnce(2);
|
|
143
|
+
input.mockResolvedValueOnce('This is a great piece of feedback that is long enough to pass validation easily.');
|
|
144
|
+
await feedbackCommand({});
|
|
145
|
+
expect(authenticatedFetch).toHaveBeenCalledWith('/feedback/recipients');
|
|
146
|
+
expect(select).toHaveBeenCalledTimes(2);
|
|
147
|
+
expect(input).toHaveBeenCalledTimes(1);
|
|
148
|
+
expect(authenticatedFetch).toHaveBeenCalledWith('/feedback', expect.objectContaining({
|
|
149
|
+
method: 'POST',
|
|
150
|
+
body: expect.stringContaining('"toUserId":1'),
|
|
151
|
+
}));
|
|
152
|
+
});
|
|
153
|
+
it('shows no recipients message when list is empty', async () => {
|
|
154
|
+
setupInteractiveMocks();
|
|
155
|
+
const recipientsResponse = {
|
|
156
|
+
ok: true,
|
|
157
|
+
json: vi.fn().mockResolvedValue([]),
|
|
158
|
+
};
|
|
159
|
+
authenticatedFetch.mockResolvedValueOnce(recipientsResponse);
|
|
160
|
+
await feedbackCommand({});
|
|
161
|
+
const logCalls = console.log.mock.calls.map((c) => String(c[0]));
|
|
162
|
+
expect(logCalls.some((msg) => msg.includes('No recipients'))).toBe(true);
|
|
163
|
+
// Should not call POST /feedback
|
|
164
|
+
expect(authenticatedFetch).toHaveBeenCalledTimes(1);
|
|
165
|
+
});
|
|
166
|
+
it('exits with error when recipients API fails', async () => {
|
|
167
|
+
setupInteractiveMocks();
|
|
168
|
+
const recipientsResponse = {
|
|
169
|
+
ok: false,
|
|
170
|
+
statusText: 'Forbidden',
|
|
171
|
+
json: vi.fn().mockResolvedValue({ error: 'Not allowed' }),
|
|
172
|
+
};
|
|
173
|
+
authenticatedFetch.mockResolvedValueOnce(recipientsResponse);
|
|
174
|
+
await expect(feedbackCommand({})).rejects.toThrow('process.exit(1)');
|
|
175
|
+
const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
|
|
176
|
+
expect(errorCalls.some((msg) => msg.includes('Not allowed'))).toBe(true);
|
|
177
|
+
});
|
|
178
|
+
it('exits with error when recipients fetch throws network error', async () => {
|
|
179
|
+
setupInteractiveMocks();
|
|
180
|
+
authenticatedFetch.mockRejectedValueOnce(new Error('Network error'));
|
|
181
|
+
await expect(feedbackCommand({})).rejects.toThrow('process.exit(1)');
|
|
182
|
+
const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
|
|
183
|
+
expect(errorCalls.some((msg) => msg.includes('Failed to connect'))).toBe(true);
|
|
184
|
+
});
|
|
185
|
+
it('re-prompts when description is too short', async () => {
|
|
186
|
+
setupInteractiveMocks();
|
|
187
|
+
const recipientsResponse = {
|
|
188
|
+
ok: true,
|
|
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);
|
|
198
|
+
select.mockResolvedValueOnce(2).mockResolvedValueOnce(3);
|
|
199
|
+
input
|
|
200
|
+
.mockResolvedValueOnce('Too short')
|
|
201
|
+
.mockResolvedValueOnce('This description is now long enough to pass the fifty character minimum validation check.');
|
|
202
|
+
await feedbackCommand({});
|
|
203
|
+
expect(input).toHaveBeenCalledTimes(2);
|
|
204
|
+
const logCalls = console.log.mock.calls.map((c) => String(c[0]));
|
|
205
|
+
expect(logCalls.some((msg) => msg.includes('Too short'))).toBe(true);
|
|
206
|
+
});
|
|
207
|
+
it('re-prompts when description is too long', async () => {
|
|
208
|
+
setupInteractiveMocks();
|
|
209
|
+
const recipientsResponse = {
|
|
210
|
+
ok: true,
|
|
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);
|
|
220
|
+
select.mockResolvedValueOnce(1).mockResolvedValueOnce(1);
|
|
221
|
+
input
|
|
222
|
+
.mockResolvedValueOnce('x'.repeat(501))
|
|
223
|
+
.mockResolvedValueOnce('This description is now exactly the right length to pass all validation checks successfully.');
|
|
224
|
+
await feedbackCommand({});
|
|
225
|
+
expect(input).toHaveBeenCalledTimes(2);
|
|
226
|
+
const logCalls = console.log.mock.calls.map((c) => String(c[0]));
|
|
227
|
+
expect(logCalls.some((msg) => msg.includes('Too long'))).toBe(true);
|
|
228
|
+
});
|
|
229
|
+
it('falls back to recipients error statusText when no error body', async () => {
|
|
230
|
+
setupInteractiveMocks();
|
|
231
|
+
const recipientsResponse = {
|
|
232
|
+
ok: false,
|
|
233
|
+
statusText: 'Service Unavailable',
|
|
234
|
+
json: vi.fn().mockResolvedValue({}),
|
|
235
|
+
};
|
|
236
|
+
authenticatedFetch.mockResolvedValueOnce(recipientsResponse);
|
|
237
|
+
await expect(feedbackCommand({})).rejects.toThrow('process.exit(1)');
|
|
238
|
+
const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
|
|
239
|
+
expect(errorCalls.some((msg) => msg.includes('Service Unavailable'))).toBe(true);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
});
|
package/dist/commands/init.js
CHANGED
|
@@ -75,6 +75,17 @@ description: >-
|
|
|
75
75
|
plan, scopes to the current feature and appends results to the plan file.
|
|
76
76
|
---
|
|
77
77
|
|
|
78
|
+
`,
|
|
79
|
+
'security-audit': `---
|
|
80
|
+
user-invocable: true
|
|
81
|
+
description: >-
|
|
82
|
+
Run a comprehensive security posture audit across the entire platform.
|
|
83
|
+
Use when the developer wants to assess security, says "security audit",
|
|
84
|
+
or "check our security posture". Covers infrastructure, encryption, auth,
|
|
85
|
+
application security, data protection, CI/CD, and compliance readiness.
|
|
86
|
+
Investigates all platform repositories for a holistic view.
|
|
87
|
+
---
|
|
88
|
+
|
|
78
89
|
`,
|
|
79
90
|
validate: `---
|
|
80
91
|
user-invocable: true
|
|
@@ -143,6 +154,11 @@ description: Run a comprehensive project compliance audit against team standards
|
|
|
143
154
|
description: Review the project's UI against usability heuristics using Chrome DevTools MCP.
|
|
144
155
|
---
|
|
145
156
|
|
|
157
|
+
`,
|
|
158
|
+
'security-audit': `---
|
|
159
|
+
description: Run a comprehensive security posture audit across the entire platform.
|
|
160
|
+
---
|
|
161
|
+
|
|
146
162
|
`,
|
|
147
163
|
validate: `---
|
|
148
164
|
description: Run the validation plan from the plan document after implementation.
|
|
@@ -302,6 +318,7 @@ export async function initCommand(options) {
|
|
|
302
318
|
'debug',
|
|
303
319
|
'audit',
|
|
304
320
|
'ux-audit',
|
|
321
|
+
'security-audit',
|
|
305
322
|
'update',
|
|
306
323
|
'help',
|
|
307
324
|
];
|
|
@@ -336,7 +353,7 @@ export async function initCommand(options) {
|
|
|
336
353
|
await fs.writeFile(path.join(skillDir, 'SKILL.md'), skillContent, 'utf-8');
|
|
337
354
|
}
|
|
338
355
|
}
|
|
339
|
-
createdFiles.push('.claude/skills/melt-{setup,plan,validate,review,pr,debug,audit,ux-audit,update,help}/SKILL.md');
|
|
356
|
+
createdFiles.push('.claude/skills/melt-{setup,plan,validate,review,pr,debug,audit,ux-audit,security-audit,update,help}/SKILL.md');
|
|
340
357
|
}
|
|
341
358
|
// Cursor files
|
|
342
359
|
if (tools.cursor) {
|
|
@@ -347,7 +364,7 @@ export async function initCommand(options) {
|
|
|
347
364
|
await fs.writeFile(path.join(cwd, `.cursor/commands/melt-${name}.md`), workflowContent, 'utf-8');
|
|
348
365
|
}
|
|
349
366
|
}
|
|
350
|
-
createdFiles.push('.cursor/commands/melt-{setup,plan,validate,review,pr,debug,audit,ux-audit,update,help}.md');
|
|
367
|
+
createdFiles.push('.cursor/commands/melt-{setup,plan,validate,review,pr,debug,audit,ux-audit,security-audit,update,help}.md');
|
|
351
368
|
}
|
|
352
369
|
// OpenCode files
|
|
353
370
|
if (tools.opencode) {
|
|
@@ -359,7 +376,7 @@ export async function initCommand(options) {
|
|
|
359
376
|
await fs.writeFile(path.join(cwd, `.opencode/commands/melt-${name}.md`), commandContent, 'utf-8');
|
|
360
377
|
}
|
|
361
378
|
}
|
|
362
|
-
createdFiles.push('.opencode/commands/melt-{setup,plan,validate,review,pr,debug,audit,ux-audit,update,help}.md');
|
|
379
|
+
createdFiles.push('.opencode/commands/melt-{setup,plan,validate,review,pr,debug,audit,ux-audit,security-audit,update,help}.md');
|
|
363
380
|
}
|
|
364
381
|
// Print summary
|
|
365
382
|
console.log(chalk.green('Created files:'));
|
|
@@ -371,7 +388,7 @@ export async function initCommand(options) {
|
|
|
371
388
|
console.log(chalk.cyan('Want support for your tool? Let us know in #dev on Slack'));
|
|
372
389
|
console.log();
|
|
373
390
|
}
|
|
374
|
-
const commandNames = 'melt-setup, melt-plan, melt-validate, melt-review, melt-pr, melt-debug, melt-audit, melt-ux-audit, melt-update, melt-help';
|
|
391
|
+
const commandNames = 'melt-setup, melt-plan, melt-validate, melt-review, melt-pr, melt-debug, melt-audit, melt-ux-audit, melt-security-audit, melt-update, melt-help';
|
|
375
392
|
if (tools.claude) {
|
|
376
393
|
console.log(chalk.dim(`Available skills: /${commandNames.replace(/, /g, ', /')}`));
|
|
377
394
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|