@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.
Files changed (38) hide show
  1. package/dist/commands/audit.js +30 -5
  2. package/dist/commands/audit.test.d.ts +1 -0
  3. package/dist/commands/audit.test.js +250 -0
  4. package/dist/commands/coins.test.d.ts +1 -0
  5. package/dist/commands/coins.test.js +133 -0
  6. package/dist/commands/feedback.test.d.ts +1 -0
  7. package/dist/commands/feedback.test.js +242 -0
  8. package/dist/commands/init.js +21 -4
  9. package/dist/commands/init.test.d.ts +1 -0
  10. package/dist/commands/init.test.js +476 -0
  11. package/dist/commands/login.test.d.ts +1 -0
  12. package/dist/commands/login.test.js +174 -0
  13. package/dist/commands/logout.test.d.ts +1 -0
  14. package/dist/commands/logout.test.js +59 -0
  15. package/dist/commands/plan.test.d.ts +1 -0
  16. package/dist/commands/plan.test.js +283 -0
  17. package/dist/commands/standup.test.d.ts +1 -0
  18. package/dist/commands/standup.test.js +252 -0
  19. package/dist/commands/templates.test.d.ts +1 -0
  20. package/dist/commands/templates.test.js +89 -0
  21. package/dist/commands/version.test.d.ts +1 -0
  22. package/dist/commands/version.test.js +86 -0
  23. package/dist/index.js +1 -1
  24. package/dist/utils/api.test.d.ts +1 -0
  25. package/dist/utils/api.test.js +96 -0
  26. package/dist/utils/auth.test.d.ts +1 -0
  27. package/dist/utils/auth.test.js +165 -0
  28. package/dist/utils/banner.test.d.ts +1 -0
  29. package/dist/utils/banner.test.js +34 -0
  30. package/dist/utils/git.test.d.ts +1 -0
  31. package/dist/utils/git.test.js +184 -0
  32. package/dist/utils/package-manager.test.d.ts +1 -0
  33. package/dist/utils/package-manager.test.js +76 -0
  34. package/dist/utils/templates.test.d.ts +1 -0
  35. package/dist/utils/templates.test.js +50 -0
  36. package/dist/utils/version-check.test.d.ts +1 -0
  37. package/dist/utils/version-check.test.js +135 -0
  38. package/package.json +2 -1
@@ -4,7 +4,12 @@ import path from 'path';
4
4
  import { getToken, tokenFetch } from '../utils/api.js';
5
5
  import { getGitBranch, getGitCommit, getGitRepository, getProjectName, findMdFiles, } from '../utils/git.js';
6
6
  function detectAuditType(filename) {
7
- return filename.toLowerCase().includes('ux-audit') ? 'ux-audit' : 'audit';
7
+ const lower = filename.toLowerCase();
8
+ if (lower.includes('security-audit'))
9
+ return 'security-audit';
10
+ if (lower.includes('ux-audit'))
11
+ return 'ux-audit';
12
+ return 'audit';
8
13
  }
9
14
  async function autoDetectAuditFile() {
10
15
  const cwd = process.cwd();
@@ -13,7 +18,7 @@ async function autoDetectAuditFile() {
13
18
  if (auditFiles.length > 0) {
14
19
  return auditFiles[0] ?? null;
15
20
  }
16
- const candidates = ['AUDIT.md', 'UX-AUDIT.md'];
21
+ const candidates = ['AUDIT.md', 'UX-AUDIT.md', 'SECURITY-AUDIT.md'];
17
22
  for (const name of candidates) {
18
23
  const filePath = path.join(cwd, name);
19
24
  if (await fs.pathExists(filePath)) {
@@ -111,6 +116,7 @@ export async function auditListCommand(options) {
111
116
  const typeLabels = {
112
117
  audit: 'Tech Audit',
113
118
  'ux-audit': 'UX Audit',
119
+ 'security-audit': 'Security',
114
120
  };
115
121
  if (options.latest) {
116
122
  console.log(chalk.bold(`\n Latest Audits (${body.count}):\n`));
@@ -126,9 +132,24 @@ export async function auditListCommand(options) {
126
132
  });
127
133
  const repo = r.repository ?? r.project;
128
134
  const label = typeLabels[r.type] ?? r.type;
129
- const typeColor = r.type === 'ux-audit' ? chalk.yellow : chalk.magenta;
135
+ const typeColor = r.type === 'ux-audit'
136
+ ? chalk.yellow
137
+ : r.type === 'security-audit'
138
+ ? chalk.red
139
+ : chalk.magenta;
130
140
  const ageText = daysAgo === 0 ? 'today' : `${daysAgo}d ago`;
131
- const ageColor = daysAgo <= 7 ? chalk.green : daysAgo <= 30 ? chalk.yellow : chalk.red;
141
+ const isSecurityAudit = r.type === 'security-audit';
142
+ const ageColor = isSecurityAudit
143
+ ? daysAgo <= 30
144
+ ? chalk.green
145
+ : daysAgo <= 90
146
+ ? chalk.yellow
147
+ : chalk.red
148
+ : daysAgo <= 7
149
+ ? chalk.green
150
+ : daysAgo <= 30
151
+ ? chalk.yellow
152
+ : chalk.red;
132
153
  console.log(` ${typeColor(label.padEnd(12))} ${chalk.white(repo.padEnd(40))} ${ageColor(ageText.padEnd(10))} ${chalk.dim(r.author.padEnd(30))} ${chalk.dim(date)}`);
133
154
  }
134
155
  }
@@ -147,7 +168,11 @@ export async function auditListCommand(options) {
147
168
  });
148
169
  const repo = r.repository ?? r.project;
149
170
  const label = typeLabels[r.type] ?? r.type;
150
- const typeColor = r.type === 'ux-audit' ? chalk.yellow : chalk.magenta;
171
+ const typeColor = r.type === 'ux-audit'
172
+ ? chalk.yellow
173
+ : r.type === 'security-audit'
174
+ ? chalk.red
175
+ : chalk.magenta;
151
176
  console.log(` ${typeColor(label.padEnd(12))} ${chalk.white(repo.padEnd(40))} ${chalk.dim(r.author.padEnd(30))} ${chalk.dim(date)}`);
152
177
  if (r.branch && r.branch !== 'main') {
153
178
  console.log(` ${' '.padEnd(12)} ${chalk.dim(`branch: ${r.branch} commit: ${r.commit ?? 'N/A'}`)}`);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,250 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ vi.mock('fs-extra', () => ({
3
+ default: {
4
+ pathExists: vi.fn(),
5
+ readFile: vi.fn(),
6
+ },
7
+ }));
8
+ vi.mock('../utils/api.js', () => ({
9
+ getToken: vi.fn(),
10
+ tokenFetch: vi.fn(),
11
+ }));
12
+ vi.mock('../utils/git.js', () => ({
13
+ getGitBranch: vi.fn(),
14
+ getGitCommit: vi.fn(),
15
+ getGitRepository: vi.fn(),
16
+ getProjectName: vi.fn(),
17
+ findMdFiles: vi.fn(),
18
+ }));
19
+ import fs from 'fs-extra';
20
+ import { getToken, tokenFetch } from '../utils/api.js';
21
+ import { getGitBranch, getGitCommit, getGitRepository, getProjectName, findMdFiles, } from '../utils/git.js';
22
+ import { auditSubmitCommand, auditListCommand } from './audit.js';
23
+ beforeEach(() => {
24
+ vi.clearAllMocks();
25
+ vi.spyOn(console, 'log').mockImplementation(() => { });
26
+ vi.spyOn(console, 'error').mockImplementation(() => { });
27
+ vi.spyOn(process, 'exit').mockImplementation((code) => {
28
+ throw new Error(`process.exit(${code})`);
29
+ });
30
+ });
31
+ describe('auditSubmitCommand', () => {
32
+ function setupGitMocks() {
33
+ ;
34
+ getToken.mockResolvedValue('test-token');
35
+ getGitBranch.mockReturnValue('main');
36
+ getGitCommit.mockReturnValue('abc1234');
37
+ getGitRepository.mockReturnValue({
38
+ slug: 'Org/Repo',
39
+ url: 'https://github.com/Org/Repo.git',
40
+ });
41
+ getProjectName.mockReturnValue('test-project');
42
+ }
43
+ it('submits audit with type "security-audit" when filename contains security-audit', async () => {
44
+ setupGitMocks();
45
+ fs.pathExists.mockResolvedValue(true);
46
+ fs.readFile.mockResolvedValue('# Security Audit\nFindings here.');
47
+ const mockResponse = {
48
+ ok: true,
49
+ json: vi.fn().mockResolvedValue({ id: 'audit-123' }),
50
+ };
51
+ tokenFetch.mockResolvedValue(mockResponse);
52
+ await auditSubmitCommand('2026-03-26-security-audit.md');
53
+ expect(tokenFetch).toHaveBeenCalledWith('test-token', '/audits', expect.objectContaining({
54
+ method: 'POST',
55
+ body: expect.stringContaining('"type":"security-audit"'),
56
+ }));
57
+ });
58
+ it('submits audit with type "ux-audit" when filename contains ux-audit', async () => {
59
+ setupGitMocks();
60
+ fs.pathExists.mockResolvedValue(true);
61
+ fs.readFile.mockResolvedValue('# UX Audit');
62
+ const mockResponse = {
63
+ ok: true,
64
+ json: vi.fn().mockResolvedValue({ id: 'audit-456' }),
65
+ };
66
+ tokenFetch.mockResolvedValue(mockResponse);
67
+ await auditSubmitCommand('UX-AUDIT.md');
68
+ expect(tokenFetch).toHaveBeenCalledWith('test-token', '/audits', expect.objectContaining({
69
+ method: 'POST',
70
+ body: expect.stringContaining('"type":"ux-audit"'),
71
+ }));
72
+ });
73
+ it('submits audit with type "audit" for generic audit filenames', async () => {
74
+ setupGitMocks();
75
+ fs.pathExists.mockResolvedValue(true);
76
+ fs.readFile.mockResolvedValue('# Audit');
77
+ const mockResponse = {
78
+ ok: true,
79
+ json: vi.fn().mockResolvedValue({ id: 'audit-789' }),
80
+ };
81
+ tokenFetch.mockResolvedValue(mockResponse);
82
+ await auditSubmitCommand('AUDIT.md');
83
+ expect(tokenFetch).toHaveBeenCalledWith('test-token', '/audits', expect.objectContaining({
84
+ method: 'POST',
85
+ body: expect.stringContaining('"type":"audit"'),
86
+ }));
87
+ });
88
+ it('submits audit with type "audit" for random filenames', async () => {
89
+ setupGitMocks();
90
+ fs.pathExists.mockResolvedValue(true);
91
+ fs.readFile.mockResolvedValue('# Random');
92
+ const mockResponse = {
93
+ ok: true,
94
+ json: vi.fn().mockResolvedValue({ id: 'audit-000' }),
95
+ };
96
+ tokenFetch.mockResolvedValue(mockResponse);
97
+ await auditSubmitCommand('random-file.md');
98
+ expect(tokenFetch).toHaveBeenCalledWith('test-token', '/audits', expect.objectContaining({
99
+ method: 'POST',
100
+ body: expect.stringContaining('"type":"audit"'),
101
+ }));
102
+ });
103
+ it('exits with error when file not found', async () => {
104
+ setupGitMocks();
105
+ fs.pathExists.mockResolvedValue(false);
106
+ await expect(auditSubmitCommand('nonexistent.md')).rejects.toThrow('process.exit(1)');
107
+ expect(console.error).toHaveBeenCalled();
108
+ });
109
+ it('sends correct payload fields', async () => {
110
+ setupGitMocks();
111
+ vi.mocked(fs.pathExists).mockResolvedValue(true);
112
+ vi.mocked(fs.readFile).mockResolvedValue('audit content here');
113
+ const mockResponse = {
114
+ ok: true,
115
+ json: vi.fn().mockResolvedValue({ id: 'audit-100' }),
116
+ };
117
+ tokenFetch.mockResolvedValue(mockResponse);
118
+ await auditSubmitCommand('AUDIT.md');
119
+ const call = tokenFetch.mock.calls[0];
120
+ const body = JSON.parse(call[2].body);
121
+ expect(body).toEqual({
122
+ type: 'audit',
123
+ project: 'test-project',
124
+ repository: 'Org/Repo',
125
+ repositoryUrl: 'https://github.com/Org/Repo.git',
126
+ branch: 'main',
127
+ commit: 'abc1234',
128
+ content: 'audit content here',
129
+ metadata: { filename: 'AUDIT.md' },
130
+ });
131
+ });
132
+ it('exits with error when API returns failure', async () => {
133
+ setupGitMocks();
134
+ fs.pathExists.mockResolvedValue(true);
135
+ fs.readFile.mockResolvedValue('content');
136
+ const mockResponse = {
137
+ ok: false,
138
+ statusText: 'Bad Request',
139
+ json: vi.fn().mockResolvedValue({ error: 'Invalid content' }),
140
+ };
141
+ tokenFetch.mockResolvedValue(mockResponse);
142
+ await expect(auditSubmitCommand('AUDIT.md')).rejects.toThrow('process.exit(1)');
143
+ });
144
+ it('auto-detects audit file from .audits/ directory when no file provided', async () => {
145
+ setupGitMocks();
146
+ findMdFiles.mockResolvedValue(['/project/.audits/2026-03-26-security-audit.md']);
147
+ fs.pathExists.mockResolvedValue(true);
148
+ fs.readFile.mockResolvedValue('auto-detected content');
149
+ const mockResponse = {
150
+ ok: true,
151
+ json: vi.fn().mockResolvedValue({ id: 'audit-auto' }),
152
+ };
153
+ tokenFetch.mockResolvedValue(mockResponse);
154
+ await auditSubmitCommand();
155
+ expect(tokenFetch).toHaveBeenCalledWith('test-token', '/audits', expect.objectContaining({
156
+ method: 'POST',
157
+ body: expect.stringContaining('"type":"security-audit"'),
158
+ }));
159
+ });
160
+ it('exits with error when no file provided and none auto-detected', async () => {
161
+ ;
162
+ getToken.mockResolvedValue('test-token');
163
+ findMdFiles.mockResolvedValue([]);
164
+ fs.pathExists.mockResolvedValue(false);
165
+ await expect(auditSubmitCommand()).rejects.toThrow('process.exit(1)');
166
+ expect(console.error).toHaveBeenCalled();
167
+ });
168
+ });
169
+ describe('auditListCommand', () => {
170
+ it('calls API with correct query params', async () => {
171
+ ;
172
+ getToken.mockResolvedValue('test-token');
173
+ const mockResponse = {
174
+ ok: true,
175
+ status: 200,
176
+ json: vi.fn().mockResolvedValue({ audits: [], count: 0 }),
177
+ };
178
+ tokenFetch.mockResolvedValue(mockResponse);
179
+ await auditListCommand({ type: 'ux-audit', repository: 'Org/Repo', limit: '5' });
180
+ expect(tokenFetch).toHaveBeenCalledWith('test-token', expect.stringContaining('/audits?'));
181
+ const url = tokenFetch.mock.calls[0][1];
182
+ expect(url).toContain('type=ux-audit');
183
+ expect(url).toContain('repository=Org%2FRepo');
184
+ expect(url).toContain('limit=5');
185
+ });
186
+ it('passes latest=true query param when option set', async () => {
187
+ ;
188
+ getToken.mockResolvedValue('test-token');
189
+ const mockResponse = {
190
+ ok: true,
191
+ status: 200,
192
+ json: vi.fn().mockResolvedValue({ audits: [], count: 0 }),
193
+ };
194
+ tokenFetch.mockResolvedValue(mockResponse);
195
+ await auditListCommand({ latest: true });
196
+ const url = tokenFetch.mock.calls[0][1];
197
+ expect(url).toContain('latest=true');
198
+ });
199
+ it('exits with error on 403 response', async () => {
200
+ ;
201
+ getToken.mockResolvedValue('test-token');
202
+ const mockResponse = {
203
+ ok: false,
204
+ status: 403,
205
+ statusText: 'Forbidden',
206
+ };
207
+ tokenFetch.mockResolvedValue(mockResponse);
208
+ await expect(auditListCommand({})).rejects.toThrow('process.exit(1)');
209
+ expect(console.error).toHaveBeenCalled();
210
+ });
211
+ it('displays audit list when audits exist', async () => {
212
+ ;
213
+ getToken.mockResolvedValue('test-token');
214
+ const mockResponse = {
215
+ ok: true,
216
+ status: 200,
217
+ json: vi.fn().mockResolvedValue({
218
+ audits: [
219
+ {
220
+ id: '1',
221
+ type: 'audit',
222
+ project: 'my-project',
223
+ repository: 'Org/Repo',
224
+ author: 'dev@meltstudio.co',
225
+ branch: 'main',
226
+ commit: 'abc1234',
227
+ createdAt: '2026-03-25T10:00:00Z',
228
+ },
229
+ ],
230
+ count: 1,
231
+ }),
232
+ };
233
+ tokenFetch.mockResolvedValue(mockResponse);
234
+ await auditListCommand({});
235
+ expect(console.log).toHaveBeenCalled();
236
+ });
237
+ it('shows "No audits found" when list is empty', async () => {
238
+ ;
239
+ getToken.mockResolvedValue('test-token');
240
+ const mockResponse = {
241
+ ok: true,
242
+ status: 200,
243
+ json: vi.fn().mockResolvedValue({ audits: [], count: 0 }),
244
+ };
245
+ tokenFetch.mockResolvedValue(mockResponse);
246
+ await auditListCommand({});
247
+ const errorCalls = console.log.mock.calls.map((c) => c[0]);
248
+ expect(errorCalls.some((msg) => typeof msg === 'string' && msg.includes('No audits found'))).toBe(true);
249
+ });
250
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,133 @@
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
+ import { isAuthenticated, authenticatedFetch } from '../utils/auth.js';
7
+ import { coinsCommand } from './coins.js';
8
+ beforeEach(() => {
9
+ vi.clearAllMocks();
10
+ vi.spyOn(console, 'log').mockImplementation(() => { });
11
+ vi.spyOn(console, 'error').mockImplementation(() => { });
12
+ vi.spyOn(process, 'exit').mockImplementation((code) => {
13
+ throw new Error(`process.exit(${code})`);
14
+ });
15
+ });
16
+ describe('coinsCommand', () => {
17
+ describe('balance (default)', () => {
18
+ it('exits with error when not authenticated', async () => {
19
+ ;
20
+ isAuthenticated.mockResolvedValue(false);
21
+ await expect(coinsCommand({})).rejects.toThrow('process.exit(1)');
22
+ expect(console.error).toHaveBeenCalled();
23
+ });
24
+ it('displays coin balance on success', async () => {
25
+ ;
26
+ isAuthenticated.mockResolvedValue(true);
27
+ const mockResponse = {
28
+ ok: true,
29
+ json: vi.fn().mockResolvedValue({ coins: 5, period: '28d' }),
30
+ };
31
+ authenticatedFetch.mockResolvedValue(mockResponse);
32
+ await coinsCommand({});
33
+ expect(authenticatedFetch).toHaveBeenCalledWith('/coins');
34
+ const logCalls = console.log.mock.calls.map((c) => String(c[0]));
35
+ expect(logCalls.some((msg) => msg.includes('5'))).toBe(true);
36
+ });
37
+ it('displays singular coin text for 1 coin', async () => {
38
+ ;
39
+ isAuthenticated.mockResolvedValue(true);
40
+ authenticatedFetch.mockResolvedValue({
41
+ ok: true,
42
+ json: vi.fn().mockResolvedValue({ coins: 1, period: '28d' }),
43
+ });
44
+ await coinsCommand({});
45
+ const logCalls = console.log.mock.calls.map((c) => String(c[0]));
46
+ expect(logCalls.some((msg) => msg.includes('coin') && !msg.includes('coins'))).toBe(true);
47
+ });
48
+ it('displays plural coin text for multiple coins', async () => {
49
+ ;
50
+ isAuthenticated.mockResolvedValue(true);
51
+ authenticatedFetch.mockResolvedValue({
52
+ ok: true,
53
+ json: vi.fn().mockResolvedValue({ coins: 3, period: '28d' }),
54
+ });
55
+ await coinsCommand({});
56
+ const logCalls = console.log.mock.calls.map((c) => String(c[0]));
57
+ expect(logCalls.some((msg) => msg.includes('coins'))).toBe(true);
58
+ });
59
+ it('exits with error when API returns failure', async () => {
60
+ ;
61
+ isAuthenticated.mockResolvedValue(true);
62
+ const mockResponse = {
63
+ ok: false,
64
+ statusText: 'Internal Server Error',
65
+ json: vi.fn().mockResolvedValue({ error: 'Something went wrong' }),
66
+ };
67
+ authenticatedFetch.mockResolvedValue(mockResponse);
68
+ await expect(coinsCommand({})).rejects.toThrow('process.exit(1)');
69
+ const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
70
+ expect(errorCalls.some((msg) => msg.includes('Something went wrong'))).toBe(true);
71
+ });
72
+ });
73
+ describe('leaderboard', () => {
74
+ it('exits with error when not authenticated', async () => {
75
+ ;
76
+ isAuthenticated.mockResolvedValue(false);
77
+ await expect(coinsCommand({ leaderboard: true })).rejects.toThrow('process.exit(1)');
78
+ });
79
+ it('displays leaderboard entries on success', async () => {
80
+ ;
81
+ isAuthenticated.mockResolvedValue(true);
82
+ const entries = [
83
+ { name: 'Alice', coins: 10 },
84
+ { name: 'Bob', coins: 7 },
85
+ { name: 'Charlie', coins: 3 },
86
+ ];
87
+ authenticatedFetch.mockResolvedValue({
88
+ ok: true,
89
+ json: vi.fn().mockResolvedValue(entries),
90
+ });
91
+ await coinsCommand({ leaderboard: true });
92
+ expect(authenticatedFetch).toHaveBeenCalledWith('/coins/leaderboard');
93
+ const logCalls = console.log.mock.calls.map((c) => String(c[0]));
94
+ expect(logCalls.some((msg) => msg.includes('Leaderboard'))).toBe(true);
95
+ expect(logCalls.some((msg) => msg.includes('Alice'))).toBe(true);
96
+ expect(logCalls.some((msg) => msg.includes('Bob'))).toBe(true);
97
+ });
98
+ it('shows message when no coins have been sent', async () => {
99
+ ;
100
+ isAuthenticated.mockResolvedValue(true);
101
+ authenticatedFetch.mockResolvedValue({
102
+ ok: true,
103
+ json: vi.fn().mockResolvedValue([]),
104
+ });
105
+ await coinsCommand({ leaderboard: true });
106
+ const logCalls = console.log.mock.calls.map((c) => String(c[0]));
107
+ expect(logCalls.some((msg) => msg.includes('No coins'))).toBe(true);
108
+ });
109
+ it('exits with error when leaderboard API fails', async () => {
110
+ ;
111
+ isAuthenticated.mockResolvedValue(true);
112
+ authenticatedFetch.mockResolvedValue({
113
+ ok: false,
114
+ statusText: 'Forbidden',
115
+ json: vi.fn().mockResolvedValue({ error: 'Access denied' }),
116
+ });
117
+ await expect(coinsCommand({ leaderboard: true })).rejects.toThrow('process.exit(1)');
118
+ const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
119
+ expect(errorCalls.some((msg) => msg.includes('Access denied'))).toBe(true);
120
+ });
121
+ it('calls /coins/leaderboard endpoint not /coins', async () => {
122
+ ;
123
+ isAuthenticated.mockResolvedValue(true);
124
+ authenticatedFetch.mockResolvedValue({
125
+ ok: true,
126
+ json: vi.fn().mockResolvedValue([{ name: 'Test', coins: 1 }]),
127
+ });
128
+ await coinsCommand({ leaderboard: true });
129
+ expect(authenticatedFetch).toHaveBeenCalledWith('/coins/leaderboard');
130
+ expect(authenticatedFetch).not.toHaveBeenCalledWith('/coins');
131
+ });
132
+ });
133
+ });
@@ -0,0 +1 @@
1
+ export {};