@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.
@@ -5,3 +5,6 @@ export declare function auditListCommand(options: {
5
5
  latest?: boolean;
6
6
  limit?: string;
7
7
  }): Promise<void>;
8
+ export declare function auditViewCommand(id: string, options: {
9
+ output?: string;
10
+ }): Promise<void>;
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meltstudio/meltctl",
3
- "version": "4.28.1",
3
+ "version": "4.29.0",
4
4
  "description": "AI-first development tools for teams - set up AGENTS.md, Claude Code, Cursor, and OpenCode standards",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",