@meltstudio/meltctl 4.30.0 → 4.32.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.
@@ -3,6 +3,8 @@ vi.mock('fs-extra', () => ({
3
3
  default: {
4
4
  pathExists: vi.fn(),
5
5
  readFile: vi.fn(),
6
+ ensureDir: vi.fn(),
7
+ writeFile: vi.fn(),
6
8
  },
7
9
  }));
8
10
  vi.mock('../utils/api.js', () => ({
@@ -19,7 +21,7 @@ vi.mock('../utils/git.js', () => ({
19
21
  import fs from 'fs-extra';
20
22
  import { getToken, tokenFetch } from '../utils/api.js';
21
23
  import { getGitBranch, getGitCommit, getGitRepository, getProjectName, findMdFiles, } from '../utils/git.js';
22
- import { auditSubmitCommand, auditListCommand } from './audit.js';
24
+ import { auditSubmitCommand, auditListCommand, auditViewCommand } from './audit.js';
23
25
  beforeEach(() => {
24
26
  vi.clearAllMocks();
25
27
  vi.spyOn(console, 'log').mockImplementation(() => { });
@@ -320,3 +322,94 @@ describe('auditListCommand', () => {
320
322
  expect(errorCalls.some((msg) => typeof msg === 'string' && msg.includes('No audits found'))).toBe(true);
321
323
  });
322
324
  });
325
+ describe('auditViewCommand', () => {
326
+ const sampleAudit = {
327
+ id: 'audit-abc123',
328
+ type: 'ux-audit',
329
+ project: 'my-project',
330
+ repository: 'Org/Repo',
331
+ author: 'dev@meltstudio.co',
332
+ branch: 'main',
333
+ commit: 'abc1234',
334
+ content: '# UX Audit\n\nFindings here.',
335
+ createdAt: '2026-03-25T10:00:00Z',
336
+ };
337
+ it('exits with "Access denied" on 403 response', async () => {
338
+ ;
339
+ getToken.mockResolvedValue('test-token');
340
+ tokenFetch.mockResolvedValue({
341
+ ok: false,
342
+ status: 403,
343
+ statusText: 'Forbidden',
344
+ });
345
+ await expect(auditViewCommand('audit-abc123', {})).rejects.toThrow('process.exit(1)');
346
+ const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
347
+ expect(errorCalls.some((msg) => msg.includes('Access denied'))).toBe(true);
348
+ });
349
+ it('exits with "Audit not found" on 404 response', async () => {
350
+ ;
351
+ getToken.mockResolvedValue('test-token');
352
+ tokenFetch.mockResolvedValue({
353
+ ok: false,
354
+ status: 404,
355
+ statusText: 'Not Found',
356
+ });
357
+ await expect(auditViewCommand('nonexistent-id', {})).rejects.toThrow('process.exit(1)');
358
+ const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
359
+ expect(errorCalls.some((msg) => msg.includes('Audit not found'))).toBe(true);
360
+ });
361
+ it('exits with error from body on non-ok response', async () => {
362
+ ;
363
+ getToken.mockResolvedValue('test-token');
364
+ tokenFetch.mockResolvedValue({
365
+ ok: false,
366
+ status: 500,
367
+ statusText: 'Internal Server Error',
368
+ json: vi.fn().mockResolvedValue({ error: 'Database unavailable' }),
369
+ });
370
+ await expect(auditViewCommand('audit-abc123', {})).rejects.toThrow('process.exit(1)');
371
+ const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
372
+ expect(errorCalls.some((msg) => msg.includes('Database unavailable'))).toBe(true);
373
+ });
374
+ it('prints audit content with header when no --output flag', async () => {
375
+ ;
376
+ getToken.mockResolvedValue('test-token');
377
+ tokenFetch.mockResolvedValue({
378
+ ok: true,
379
+ status: 200,
380
+ json: vi.fn().mockResolvedValue(sampleAudit),
381
+ });
382
+ await auditViewCommand('audit-abc123', {});
383
+ const logCalls = console.log.mock.calls.map((c) => String(c[0]));
384
+ // Header should contain type label, repo, author, and date
385
+ expect(logCalls.some((msg) => msg.includes('UX Audit'))).toBe(true);
386
+ expect(logCalls.some((msg) => msg.includes('Org/Repo'))).toBe(true);
387
+ expect(logCalls.some((msg) => msg.includes('dev@meltstudio.co'))).toBe(true);
388
+ // Content should be printed
389
+ expect(logCalls.some((msg) => msg.includes('# UX Audit'))).toBe(true);
390
+ });
391
+ it('writes content to file with --output flag', async () => {
392
+ ;
393
+ getToken.mockResolvedValue('test-token');
394
+ tokenFetch.mockResolvedValue({
395
+ ok: true,
396
+ status: 200,
397
+ json: vi.fn().mockResolvedValue(sampleAudit),
398
+ });
399
+ fs.ensureDir.mockResolvedValue(undefined);
400
+ fs.writeFile.mockResolvedValue(undefined);
401
+ await auditViewCommand('audit-abc123', { output: 'output/audit.md' });
402
+ expect(fs.ensureDir).toHaveBeenCalled();
403
+ expect(fs.writeFile).toHaveBeenCalledWith(expect.stringContaining('audit.md'), '# UX Audit\n\nFindings here.', 'utf-8');
404
+ const logCalls = console.log.mock.calls.map((c) => String(c[0]));
405
+ expect(logCalls.some((msg) => msg.includes('Audit saved to'))).toBe(true);
406
+ });
407
+ it('exits with error message on network error', async () => {
408
+ ;
409
+ getToken.mockResolvedValue('test-token');
410
+ tokenFetch.mockRejectedValue(new Error('Network error'));
411
+ await expect(auditViewCommand('audit-abc123', {})).rejects.toThrow('process.exit(1)');
412
+ const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
413
+ expect(errorCalls.some((msg) => msg.includes('Network error'))).toBe(true);
414
+ });
415
+ });
@@ -117,6 +117,16 @@ description: >-
117
117
  This is a reference skill — it explains the process, not executes it.
118
118
  ---
119
119
 
120
+ `,
121
+ link: `---
122
+ user-invocable: true
123
+ description: >-
124
+ Connect and verify required integrations (ticket tracker, browser
125
+ testing). Use after melt-setup, when tools aren't working, or when
126
+ a skill reports missing MCP connections. Guides setup, verifies
127
+ each tool works, and updates AGENTS.md connection status.
128
+ ---
129
+
120
130
  `,
121
131
  };
122
132
  const OPENCODE_COMMAND_FRONTMATTER = {
@@ -174,6 +184,11 @@ description: Update Melt skills and standards to the latest version.
174
184
  description: Answer questions about the AI-First Development Playbook and team workflow.
175
185
  ---
176
186
 
187
+ `,
188
+ link: `---
189
+ description: Connect and verify required integrations (ticket tracker, browser testing).
190
+ ---
191
+
177
192
  `,
178
193
  };
179
194
  const GITIGNORE_ENTRIES = ['.env.local', '.claude/settings.local.json'];
@@ -321,6 +336,7 @@ export async function initCommand(options) {
321
336
  'security-audit',
322
337
  'update',
323
338
  'help',
339
+ 'link',
324
340
  ];
325
341
  // Shared files (skip on re-init)
326
342
  if (!isReInit) {
@@ -443,7 +459,7 @@ export async function initCommand(options) {
443
459
  }
444
460
  console.log();
445
461
  console.log(chalk.dim(' The setup skill will analyze your project and fill in AGENTS.md.'));
446
- console.log(chalk.dim(' Once it finishes, commit the changes to share with your team.'));
462
+ console.log(chalk.dim(' Then run /melt-link to connect your ticket tracker and browser testing tools.'));
447
463
  console.log();
448
464
  }
449
465
  console.log(chalk.green('Done!'));
@@ -36,6 +36,7 @@ const MOCK_TEMPLATES = {
36
36
  'workflows/security-audit.md': '# Security Audit Workflow\nSecurity check.',
37
37
  'workflows/update.md': '# Update Workflow\nUpdate skills.',
38
38
  'workflows/help.md': '# Help Workflow\nAnswer questions.',
39
+ 'workflows/link.md': '# Link Workflow\nConnect tools.',
39
40
  'claude-settings.json': '{"permissions":{"allow":["Read"],"deny":["Bash"]}}',
40
41
  'mcp-configs/base.json': '{"mcpServers":{"context7":{"command":"npx","args":["context7"]}}}',
41
42
  };
@@ -114,13 +115,13 @@ describe('initCommand', () => {
114
115
  // Verify ensureDir was called for skill directories
115
116
  const ensureDirCalls = fs.ensureDir.mock.calls;
116
117
  const skillDirs = ensureDirCalls.filter(c => c[0].includes('.claude/skills/melt-'));
117
- expect(skillDirs.length).toBeGreaterThanOrEqual(11);
118
+ expect(skillDirs.length).toBeGreaterThanOrEqual(12);
118
119
  });
119
120
  it('creates all 11 Claude skills', async () => {
120
121
  await initCommand({ claude: true });
121
122
  const writeCalls = fs.writeFile.mock.calls;
122
123
  const skillFiles = writeCalls.filter(c => c[0].includes('.claude/skills/melt-') && c[0].endsWith('SKILL.md'));
123
- expect(skillFiles.length).toBe(11);
124
+ expect(skillFiles.length).toBe(12);
124
125
  const skillNames = skillFiles.map(c => {
125
126
  const match = c[0].match(/\.claude\/skills\/melt-([^/]+)/);
126
127
  return match ? match[1] : '';
@@ -136,12 +137,13 @@ describe('initCommand', () => {
136
137
  expect(skillNames).toContain('security-audit');
137
138
  expect(skillNames).toContain('update');
138
139
  expect(skillNames).toContain('help');
140
+ expect(skillNames).toContain('link');
139
141
  });
140
142
  it('creates Cursor commands without frontmatter', async () => {
141
143
  await initCommand({ cursor: true });
142
144
  const writeCalls = fs.writeFile.mock.calls;
143
145
  const cursorFiles = writeCalls.filter(c => c[0].includes('.cursor/commands/melt-'));
144
- expect(cursorFiles.length).toBe(11);
146
+ expect(cursorFiles.length).toBe(12);
145
147
  // Cursor commands should NOT have frontmatter
146
148
  const setupCmd = cursorFiles.find(c => c[0].includes('melt-setup.md'));
147
149
  expect(setupCmd).toBeDefined();
@@ -152,7 +154,7 @@ describe('initCommand', () => {
152
154
  await initCommand({ opencode: true });
153
155
  const writeCalls = fs.writeFile.mock.calls;
154
156
  const opencodeFiles = writeCalls.filter(c => c[0].includes('.opencode/commands/melt-'));
155
- expect(opencodeFiles.length).toBe(11);
157
+ expect(opencodeFiles.length).toBe(12);
156
158
  // OpenCode commands should have shorter frontmatter
157
159
  const setupCmd = opencodeFiles.find(c => c[0].includes('melt-setup.md'));
158
160
  expect(setupCmd).toBeDefined();
package/dist/index.js CHANGED
@@ -32,6 +32,7 @@ program
32
32
  .addHelpText('after', `
33
33
  ${chalk.bold('AI Skills')} ${chalk.dim('(run these in your AI coding tool, not the CLI):')}
34
34
  ${chalk.dim(' /melt-setup Analyze the project and customize AGENTS.md')}
35
+ ${chalk.dim(' /melt-link Connect and verify ticket tracker + browser testing')}
35
36
  ${chalk.dim(' /melt-plan Design an implementation approach before writing code')}
36
37
  ${chalk.dim(' /melt-validate Run the validation plan and verify end-to-end')}
37
38
  ${chalk.dim(' /melt-review Review changes against project standards')}
@@ -64,6 +65,7 @@ const project = program
64
65
  .addHelpText('after', `
65
66
  ${chalk.dim('Related skills (run in your AI coding tool after init):')}
66
67
  ${chalk.dim(' /melt-setup Analyze the project and customize AGENTS.md')}
68
+ ${chalk.dim(' /melt-link Connect and verify ticket tracker + browser testing')}
67
69
  ${chalk.dim(' /melt-update Update Melt skills to the latest version')}
68
70
  `);
69
71
  project
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meltstudio/meltctl",
3
- "version": "4.30.0",
3
+ "version": "4.32.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",