@meltstudio/meltctl 4.28.1 → 4.29.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.d.ts +3 -0
- package/dist/commands/audit.js +48 -0
- package/dist/commands/audit.test.js +72 -0
- package/dist/commands/login.test.js +20 -0
- package/dist/index.js +9 -1
- package/package.json +1 -1
package/dist/commands/audit.d.ts
CHANGED
package/dist/commands/audit.js
CHANGED
|
@@ -186,3 +186,51 @@ export async function auditListCommand(options) {
|
|
|
186
186
|
process.exit(1);
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
|
+
export async function auditViewCommand(id, options) {
|
|
190
|
+
const token = await getToken();
|
|
191
|
+
try {
|
|
192
|
+
const res = await tokenFetch(token, `/audits/${id}`);
|
|
193
|
+
if (res.status === 403) {
|
|
194
|
+
console.error(chalk.red('Access denied. Only Team Managers can view audits.'));
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
if (res.status === 404) {
|
|
198
|
+
console.error(chalk.red(`Audit not found: ${id}`));
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
if (!res.ok) {
|
|
202
|
+
const body = (await res.json());
|
|
203
|
+
console.error(chalk.red(`Failed to fetch audit: ${body.error ?? res.statusText}`));
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
const audit = (await res.json());
|
|
207
|
+
if (options.output) {
|
|
208
|
+
const outputPath = path.resolve(options.output);
|
|
209
|
+
await fs.ensureDir(path.dirname(outputPath));
|
|
210
|
+
await fs.writeFile(outputPath, audit.content, 'utf-8');
|
|
211
|
+
console.log(chalk.green(`\n ✓ Audit saved to ${path.relative(process.cwd(), outputPath)}\n`));
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
const typeLabels = {
|
|
215
|
+
audit: 'Tech Audit',
|
|
216
|
+
'ux-audit': 'UX Audit',
|
|
217
|
+
'security-audit': 'Security Audit',
|
|
218
|
+
};
|
|
219
|
+
const label = typeLabels[audit.type] ?? audit.type;
|
|
220
|
+
const date = new Date(audit.createdAt).toLocaleDateString('en-US', {
|
|
221
|
+
month: 'short',
|
|
222
|
+
day: 'numeric',
|
|
223
|
+
year: 'numeric',
|
|
224
|
+
});
|
|
225
|
+
const repo = audit.repository ?? audit.project;
|
|
226
|
+
console.log();
|
|
227
|
+
console.log(chalk.dim(` ${label} · ${repo} · ${audit.author} · ${date}`));
|
|
228
|
+
console.log();
|
|
229
|
+
console.log(audit.content);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
console.error(chalk.red(`Failed to fetch audit: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
@@ -234,6 +234,78 @@ describe('auditListCommand', () => {
|
|
|
234
234
|
await auditListCommand({});
|
|
235
235
|
expect(console.log).toHaveBeenCalled();
|
|
236
236
|
});
|
|
237
|
+
it('displays age-colored output when latest option is set', async () => {
|
|
238
|
+
;
|
|
239
|
+
getToken.mockResolvedValue('test-token');
|
|
240
|
+
const now = Date.now();
|
|
241
|
+
const threeDaysAgo = new Date(now - 3 * 24 * 60 * 60 * 1000).toISOString();
|
|
242
|
+
const fifteenDaysAgo = new Date(now - 15 * 24 * 60 * 60 * 1000).toISOString();
|
|
243
|
+
const sixtyDaysAgo = new Date(now - 60 * 24 * 60 * 60 * 1000).toISOString();
|
|
244
|
+
const mockResponse = {
|
|
245
|
+
ok: true,
|
|
246
|
+
status: 200,
|
|
247
|
+
json: vi.fn().mockResolvedValue({
|
|
248
|
+
audits: [
|
|
249
|
+
{
|
|
250
|
+
id: '1',
|
|
251
|
+
type: 'audit',
|
|
252
|
+
project: 'project-a',
|
|
253
|
+
repository: 'Org/RepoA',
|
|
254
|
+
author: 'dev1@meltstudio.co',
|
|
255
|
+
branch: 'main',
|
|
256
|
+
commit: 'aaa1111',
|
|
257
|
+
createdAt: threeDaysAgo,
|
|
258
|
+
created_at: threeDaysAgo,
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
id: '2',
|
|
262
|
+
type: 'ux-audit',
|
|
263
|
+
project: 'project-b',
|
|
264
|
+
repository: 'Org/RepoB',
|
|
265
|
+
author: 'dev2@meltstudio.co',
|
|
266
|
+
branch: 'main',
|
|
267
|
+
commit: 'bbb2222',
|
|
268
|
+
createdAt: fifteenDaysAgo,
|
|
269
|
+
created_at: fifteenDaysAgo,
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
id: '3',
|
|
273
|
+
type: 'security-audit',
|
|
274
|
+
project: 'project-c',
|
|
275
|
+
repository: 'Org/RepoC',
|
|
276
|
+
author: 'dev3@meltstudio.co',
|
|
277
|
+
branch: 'main',
|
|
278
|
+
commit: 'ccc3333',
|
|
279
|
+
createdAt: sixtyDaysAgo,
|
|
280
|
+
created_at: sixtyDaysAgo,
|
|
281
|
+
},
|
|
282
|
+
],
|
|
283
|
+
count: 3,
|
|
284
|
+
}),
|
|
285
|
+
};
|
|
286
|
+
tokenFetch.mockResolvedValue(mockResponse);
|
|
287
|
+
await auditListCommand({ latest: true });
|
|
288
|
+
const logCalls = console.log.mock.calls.map((c) => String(c[0]));
|
|
289
|
+
// Should display "Latest Audits" header
|
|
290
|
+
expect(logCalls.some((msg) => msg.includes('Latest Audits'))).toBe(true);
|
|
291
|
+
// Should display AGE column header
|
|
292
|
+
expect(logCalls.some((msg) => msg.includes('AGE'))).toBe(true);
|
|
293
|
+
});
|
|
294
|
+
it('exits with error when auditListCommand tokenFetch throws', async () => {
|
|
295
|
+
;
|
|
296
|
+
getToken.mockResolvedValue('test-token');
|
|
297
|
+
tokenFetch.mockRejectedValue(new Error('Network error'));
|
|
298
|
+
await expect(auditListCommand({})).rejects.toThrow('process.exit(1)');
|
|
299
|
+
const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
|
|
300
|
+
expect(errorCalls.some((msg) => msg.includes('Network error'))).toBe(true);
|
|
301
|
+
});
|
|
302
|
+
it('exits with error when findMdFiles throws (e.g. .audits/ unreadable)', async () => {
|
|
303
|
+
;
|
|
304
|
+
getToken.mockResolvedValue('test-token');
|
|
305
|
+
findMdFiles.mockRejectedValue(new Error('EACCES: permission denied'));
|
|
306
|
+
fs.pathExists.mockResolvedValue(false);
|
|
307
|
+
await expect(auditSubmitCommand()).rejects.toThrow();
|
|
308
|
+
});
|
|
237
309
|
it('shows "No audits found" when list is empty', async () => {
|
|
238
310
|
;
|
|
239
311
|
getToken.mockResolvedValue('test-token');
|
|
@@ -151,6 +151,26 @@ describe('loginCommand', () => {
|
|
|
151
151
|
expect(res._statusCode).toBe(200);
|
|
152
152
|
expect(res._body).toContain('failed');
|
|
153
153
|
});
|
|
154
|
+
it('handles token response with missing fields gracefully', async () => {
|
|
155
|
+
// Token response missing expiresAt
|
|
156
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
157
|
+
ok: true,
|
|
158
|
+
json: vi.fn().mockResolvedValue({ token: 'jwt-token', email: 'dev@meltstudio.co' }),
|
|
159
|
+
});
|
|
160
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
161
|
+
const loginPromise = loginCommand();
|
|
162
|
+
await new Promise(r => setTimeout(r, 10));
|
|
163
|
+
const req = createMockReq('/?code=test-code');
|
|
164
|
+
const res = createMockRes();
|
|
165
|
+
capturedRequestHandler(req, res);
|
|
166
|
+
await loginPromise;
|
|
167
|
+
// storeAuth is called with undefined for expiresAt since the field is missing
|
|
168
|
+
expect(storeAuth).toHaveBeenCalledWith({
|
|
169
|
+
token: 'jwt-token',
|
|
170
|
+
email: 'dev@meltstudio.co',
|
|
171
|
+
expiresAt: undefined,
|
|
172
|
+
});
|
|
173
|
+
});
|
|
154
174
|
it('returns 400 when callback has no code or error', async () => {
|
|
155
175
|
const fetchMock = vi.fn().mockResolvedValue({
|
|
156
176
|
ok: true,
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ import { versionCheckCommand } from './commands/version.js';
|
|
|
14
14
|
import { standupCommand } from './commands/standup.js';
|
|
15
15
|
import { feedbackCommand } from './commands/feedback.js';
|
|
16
16
|
import { coinsCommand } from './commands/coins.js';
|
|
17
|
-
import { auditSubmitCommand, auditListCommand } from './commands/audit.js';
|
|
17
|
+
import { auditSubmitCommand, auditListCommand, auditViewCommand } from './commands/audit.js';
|
|
18
18
|
import { planSubmitCommand, planListCommand } from './commands/plan.js';
|
|
19
19
|
// Read version from package.json
|
|
20
20
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -109,6 +109,14 @@ audit
|
|
|
109
109
|
.action(async (options) => {
|
|
110
110
|
await auditListCommand(options);
|
|
111
111
|
});
|
|
112
|
+
audit
|
|
113
|
+
.command('view')
|
|
114
|
+
.description('view a submitted audit by ID (Team Managers only)')
|
|
115
|
+
.argument('<id>', 'audit ID')
|
|
116
|
+
.option('-o, --output <file>', 'save to file instead of printing')
|
|
117
|
+
.action(async (id, options) => {
|
|
118
|
+
await auditViewCommand(id, options);
|
|
119
|
+
});
|
|
112
120
|
const plan = program.command('plan').description('submit and list plans');
|
|
113
121
|
plan
|
|
114
122
|
.command('submit')
|
package/package.json
CHANGED