@meltstudio/meltctl 4.23.1 → 4.24.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/init.js +20 -4
- package/dist/commands/report.d.ts +7 -0
- package/dist/commands/report.js +295 -0
- package/dist/index.js +25 -0
- package/package.json +1 -1
package/dist/commands/init.js
CHANGED
|
@@ -65,6 +65,16 @@ description: >-
|
|
|
65
65
|
report with scores and actionable fixes.
|
|
66
66
|
---
|
|
67
67
|
|
|
68
|
+
`,
|
|
69
|
+
'ux-audit': `---
|
|
70
|
+
user-invocable: true
|
|
71
|
+
description: >-
|
|
72
|
+
Review the project's UI against usability heuristics using Chrome DevTools
|
|
73
|
+
MCP. Use when the developer wants to check UX quality, says "review the UI",
|
|
74
|
+
or "UX audit". In full audit mode, crawls the entire app. During an active
|
|
75
|
+
plan, scopes to the current feature and appends results to the plan file.
|
|
76
|
+
---
|
|
77
|
+
|
|
68
78
|
`,
|
|
69
79
|
validate: `---
|
|
70
80
|
user-invocable: true
|
|
@@ -128,6 +138,11 @@ description: Systematically investigate and fix bugs.
|
|
|
128
138
|
description: Run a comprehensive project compliance audit against team standards.
|
|
129
139
|
---
|
|
130
140
|
|
|
141
|
+
`,
|
|
142
|
+
'ux-audit': `---
|
|
143
|
+
description: Review the project's UI against usability heuristics using Chrome DevTools MCP.
|
|
144
|
+
---
|
|
145
|
+
|
|
131
146
|
`,
|
|
132
147
|
validate: `---
|
|
133
148
|
description: Run the validation plan from the plan document after implementation.
|
|
@@ -286,6 +301,7 @@ export async function initCommand(options) {
|
|
|
286
301
|
'pr',
|
|
287
302
|
'debug',
|
|
288
303
|
'audit',
|
|
304
|
+
'ux-audit',
|
|
289
305
|
'update',
|
|
290
306
|
'help',
|
|
291
307
|
];
|
|
@@ -320,7 +336,7 @@ export async function initCommand(options) {
|
|
|
320
336
|
await fs.writeFile(path.join(skillDir, 'SKILL.md'), skillContent, 'utf-8');
|
|
321
337
|
}
|
|
322
338
|
}
|
|
323
|
-
createdFiles.push('.claude/skills/melt-{setup,plan,validate,review,pr,debug,audit,update,help}/SKILL.md');
|
|
339
|
+
createdFiles.push('.claude/skills/melt-{setup,plan,validate,review,pr,debug,audit,ux-audit,update,help}/SKILL.md');
|
|
324
340
|
}
|
|
325
341
|
// Cursor files
|
|
326
342
|
if (tools.cursor) {
|
|
@@ -331,7 +347,7 @@ export async function initCommand(options) {
|
|
|
331
347
|
await fs.writeFile(path.join(cwd, `.cursor/commands/melt-${name}.md`), workflowContent, 'utf-8');
|
|
332
348
|
}
|
|
333
349
|
}
|
|
334
|
-
createdFiles.push('.cursor/commands/melt-{setup,plan,validate,review,pr,debug,audit,update,help}.md');
|
|
350
|
+
createdFiles.push('.cursor/commands/melt-{setup,plan,validate,review,pr,debug,audit,ux-audit,update,help}.md');
|
|
335
351
|
}
|
|
336
352
|
// OpenCode files
|
|
337
353
|
if (tools.opencode) {
|
|
@@ -343,7 +359,7 @@ export async function initCommand(options) {
|
|
|
343
359
|
await fs.writeFile(path.join(cwd, `.opencode/commands/melt-${name}.md`), commandContent, 'utf-8');
|
|
344
360
|
}
|
|
345
361
|
}
|
|
346
|
-
createdFiles.push('.opencode/commands/melt-{setup,plan,validate,review,pr,debug,audit,update,help}.md');
|
|
362
|
+
createdFiles.push('.opencode/commands/melt-{setup,plan,validate,review,pr,debug,audit,ux-audit,update,help}.md');
|
|
347
363
|
}
|
|
348
364
|
// Print summary
|
|
349
365
|
console.log(chalk.green('Created files:'));
|
|
@@ -355,7 +371,7 @@ export async function initCommand(options) {
|
|
|
355
371
|
console.log(chalk.cyan('Want support for your tool? Let us know in #dev on Slack'));
|
|
356
372
|
console.log();
|
|
357
373
|
}
|
|
358
|
-
const commandNames = 'melt-setup, melt-plan, melt-validate, melt-review, melt-pr, melt-debug, melt-audit, melt-update, melt-help';
|
|
374
|
+
const commandNames = 'melt-setup, melt-plan, melt-validate, melt-review, melt-pr, melt-debug, melt-audit, melt-ux-audit, melt-update, melt-help';
|
|
359
375
|
if (tools.claude) {
|
|
360
376
|
console.log(chalk.dim(`Available skills: /${commandNames.replace(/, /g, ', /')}`));
|
|
361
377
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function reportPlanCommand(file?: string): Promise<void>;
|
|
2
|
+
export declare function reportAuditCommand(file?: string): Promise<void>;
|
|
3
|
+
export declare function reportListCommand(options: {
|
|
4
|
+
type?: string;
|
|
5
|
+
repository?: string;
|
|
6
|
+
limit?: string;
|
|
7
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
import { getStoredAuth, API_BASE } from '../utils/auth.js';
|
|
6
|
+
async function getToken() {
|
|
7
|
+
const envToken = process.env['MELTCTL_TOKEN'];
|
|
8
|
+
if (envToken) {
|
|
9
|
+
return envToken;
|
|
10
|
+
}
|
|
11
|
+
const auth = await getStoredAuth();
|
|
12
|
+
if (!auth) {
|
|
13
|
+
console.error(chalk.red('Not authenticated. Run `npx @meltstudio/meltctl@latest login` or set MELTCTL_TOKEN for CI.'));
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
if (new Date(auth.expiresAt) <= new Date()) {
|
|
17
|
+
console.error(chalk.red('Session expired. Run `npx @meltstudio/meltctl@latest login` to re-authenticate, or set MELTCTL_TOKEN for CI.'));
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
return auth.token;
|
|
21
|
+
}
|
|
22
|
+
async function tokenFetch(token, urlPath, options = {}) {
|
|
23
|
+
const response = await fetch(`${API_BASE}${urlPath}`, {
|
|
24
|
+
...options,
|
|
25
|
+
headers: {
|
|
26
|
+
Authorization: `Bearer ${token}`,
|
|
27
|
+
...options.headers,
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
if (response.status === 401) {
|
|
31
|
+
throw new Error('Authentication failed. Run `npx @meltstudio/meltctl@latest login` or check your MELTCTL_TOKEN.');
|
|
32
|
+
}
|
|
33
|
+
return response;
|
|
34
|
+
}
|
|
35
|
+
function getGitBranch() {
|
|
36
|
+
try {
|
|
37
|
+
return execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf-8' }).trim();
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return 'unknown';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function getGitCommit() {
|
|
44
|
+
try {
|
|
45
|
+
return execSync('git rev-parse --short HEAD', { encoding: 'utf-8' }).trim();
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return 'unknown';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function getGitRepository() {
|
|
52
|
+
try {
|
|
53
|
+
const url = execSync('git remote get-url origin', { encoding: 'utf-8' }).trim();
|
|
54
|
+
// Extract owner/repo from various URL formats:
|
|
55
|
+
// git@github.com:Owner/Repo.git -> Owner/Repo
|
|
56
|
+
// https://github.com/Owner/Repo.git -> Owner/Repo
|
|
57
|
+
// git@gitlab.com:Owner/Repo.git -> Owner/Repo
|
|
58
|
+
const match = url.match(/[/:]([\w.-]+\/[\w.-]+?)(?:\.git)?$/);
|
|
59
|
+
const slug = match ? match[1] : url;
|
|
60
|
+
return { slug, url };
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function getProjectName() {
|
|
67
|
+
const cwd = process.cwd();
|
|
68
|
+
try {
|
|
69
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
70
|
+
if (fs.pathExistsSync(pkgPath)) {
|
|
71
|
+
const pkg = fs.readJsonSync(pkgPath);
|
|
72
|
+
if (pkg.name) {
|
|
73
|
+
return pkg.name;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// fall through
|
|
79
|
+
}
|
|
80
|
+
return path.basename(cwd);
|
|
81
|
+
}
|
|
82
|
+
function extractTicketId(branch) {
|
|
83
|
+
const match = branch.match(/([A-Z]+-\d+)/i);
|
|
84
|
+
return match ? match[1] : null;
|
|
85
|
+
}
|
|
86
|
+
async function findMdFiles(dir) {
|
|
87
|
+
if (!(await fs.pathExists(dir))) {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
const results = [];
|
|
91
|
+
async function walk(current) {
|
|
92
|
+
const entries = await fs.readdir(current, { withFileTypes: true });
|
|
93
|
+
for (const entry of entries) {
|
|
94
|
+
const fullPath = path.join(current, entry.name);
|
|
95
|
+
if (entry.isDirectory()) {
|
|
96
|
+
await walk(fullPath);
|
|
97
|
+
}
|
|
98
|
+
else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
99
|
+
const stat = await fs.stat(fullPath);
|
|
100
|
+
results.push({ path: fullPath, mtime: stat.mtimeMs });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
await walk(dir);
|
|
105
|
+
results.sort((a, b) => b.mtime - a.mtime);
|
|
106
|
+
return results.map(r => r.path);
|
|
107
|
+
}
|
|
108
|
+
async function autoDetectPlanFile() {
|
|
109
|
+
const cwd = process.cwd();
|
|
110
|
+
const plansDir = path.join(cwd, '.plans');
|
|
111
|
+
const mdFiles = await findMdFiles(plansDir);
|
|
112
|
+
if (mdFiles.length === 0) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
const branch = getGitBranch();
|
|
116
|
+
const ticketId = extractTicketId(branch);
|
|
117
|
+
if (ticketId) {
|
|
118
|
+
const ticketLower = ticketId.toLowerCase();
|
|
119
|
+
const match = mdFiles.find(f => path.basename(f).toLowerCase().includes(ticketLower));
|
|
120
|
+
if (match) {
|
|
121
|
+
return match;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return mdFiles[0] ?? null;
|
|
125
|
+
}
|
|
126
|
+
async function autoDetectAuditFile() {
|
|
127
|
+
const cwd = process.cwd();
|
|
128
|
+
// Check .audits/ directory first (most recent file)
|
|
129
|
+
const auditsDir = path.join(cwd, '.audits');
|
|
130
|
+
const auditFiles = await findMdFiles(auditsDir);
|
|
131
|
+
if (auditFiles.length > 0) {
|
|
132
|
+
return auditFiles[0] ?? null;
|
|
133
|
+
}
|
|
134
|
+
// Fall back to legacy locations
|
|
135
|
+
const candidates = ['AUDIT.md', 'UX-AUDIT.md'];
|
|
136
|
+
for (const name of candidates) {
|
|
137
|
+
const filePath = path.join(cwd, name);
|
|
138
|
+
if (await fs.pathExists(filePath)) {
|
|
139
|
+
return filePath;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
function buildReportPayload(type, content, filename) {
|
|
145
|
+
const project = getProjectName();
|
|
146
|
+
const branch = getGitBranch();
|
|
147
|
+
const commit = getGitCommit();
|
|
148
|
+
const repo = getGitRepository();
|
|
149
|
+
return {
|
|
150
|
+
type,
|
|
151
|
+
project,
|
|
152
|
+
repository: repo?.slug ?? null,
|
|
153
|
+
repositoryUrl: repo?.url ?? null,
|
|
154
|
+
branch,
|
|
155
|
+
commit,
|
|
156
|
+
content,
|
|
157
|
+
metadata: { filename },
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
export async function reportPlanCommand(file) {
|
|
161
|
+
const token = await getToken();
|
|
162
|
+
let filePath;
|
|
163
|
+
if (file) {
|
|
164
|
+
filePath = path.resolve(file);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
const detected = await autoDetectPlanFile();
|
|
168
|
+
if (!detected) {
|
|
169
|
+
console.error(chalk.red('No plan file found. Provide a file path or create a plan in the .plans/ directory.'));
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
filePath = detected;
|
|
173
|
+
console.log(chalk.dim(`Auto-detected plan file: ${path.relative(process.cwd(), filePath)}`));
|
|
174
|
+
}
|
|
175
|
+
if (!(await fs.pathExists(filePath))) {
|
|
176
|
+
console.error(chalk.red(`File not found: ${filePath}`));
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
180
|
+
const filename = path.basename(filePath);
|
|
181
|
+
const payload = buildReportPayload('plan', content, filename);
|
|
182
|
+
try {
|
|
183
|
+
const res = await tokenFetch(token, '/reports', {
|
|
184
|
+
method: 'POST',
|
|
185
|
+
headers: { 'Content-Type': 'application/json' },
|
|
186
|
+
body: JSON.stringify(payload),
|
|
187
|
+
});
|
|
188
|
+
if (res.ok) {
|
|
189
|
+
const body = (await res.json());
|
|
190
|
+
console.log(chalk.green(`\n ✓ Plan report submitted! Report ID: ${body.id}\n`));
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
const body = (await res.json());
|
|
194
|
+
console.error(chalk.red(`\nFailed to submit plan report: ${body.error ?? res.statusText}`));
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
console.error(chalk.red(`Failed to submit plan report: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
export async function reportAuditCommand(file) {
|
|
204
|
+
const token = await getToken();
|
|
205
|
+
let filePath;
|
|
206
|
+
if (file) {
|
|
207
|
+
filePath = path.resolve(file);
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
const detected = await autoDetectAuditFile();
|
|
211
|
+
if (!detected) {
|
|
212
|
+
console.error(chalk.red('No audit file found. Provide a file path or create AUDIT.md or UX-AUDIT.md in the project root.'));
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
filePath = detected;
|
|
216
|
+
console.log(chalk.dim(`Auto-detected audit file: ${path.relative(process.cwd(), filePath)}`));
|
|
217
|
+
}
|
|
218
|
+
if (!(await fs.pathExists(filePath))) {
|
|
219
|
+
console.error(chalk.red(`File not found: ${filePath}`));
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
223
|
+
const filename = path.basename(filePath);
|
|
224
|
+
const payload = buildReportPayload('audit', content, filename);
|
|
225
|
+
try {
|
|
226
|
+
const res = await tokenFetch(token, '/reports', {
|
|
227
|
+
method: 'POST',
|
|
228
|
+
headers: { 'Content-Type': 'application/json' },
|
|
229
|
+
body: JSON.stringify(payload),
|
|
230
|
+
});
|
|
231
|
+
if (res.ok) {
|
|
232
|
+
const body = (await res.json());
|
|
233
|
+
console.log(chalk.green(`\n ✓ Audit report submitted! Report ID: ${body.id}\n`));
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
const body = (await res.json());
|
|
237
|
+
console.error(chalk.red(`\nFailed to submit audit report: ${body.error ?? res.statusText}`));
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
console.error(chalk.red(`Failed to submit audit report: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
export async function reportListCommand(options) {
|
|
247
|
+
const token = await getToken();
|
|
248
|
+
const params = new URLSearchParams();
|
|
249
|
+
if (options.type)
|
|
250
|
+
params.set('type', options.type);
|
|
251
|
+
if (options.repository)
|
|
252
|
+
params.set('repository', options.repository);
|
|
253
|
+
if (options.limit)
|
|
254
|
+
params.set('limit', options.limit);
|
|
255
|
+
const query = params.toString();
|
|
256
|
+
const urlPath = `/reports${query ? `?${query}` : ''}`;
|
|
257
|
+
try {
|
|
258
|
+
const res = await tokenFetch(token, urlPath);
|
|
259
|
+
if (res.status === 403) {
|
|
260
|
+
console.error(chalk.red('Access denied. Only Team Managers can list reports.'));
|
|
261
|
+
process.exit(1);
|
|
262
|
+
}
|
|
263
|
+
if (!res.ok) {
|
|
264
|
+
const body = (await res.json());
|
|
265
|
+
console.error(chalk.red(`Failed to list reports: ${body.error ?? res.statusText}`));
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
const body = (await res.json());
|
|
269
|
+
if (body.reports.length === 0) {
|
|
270
|
+
console.log(chalk.dim('\n No reports found.\n'));
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
console.log(chalk.bold(`\n Reports (${body.count}):\n`));
|
|
274
|
+
for (const r of body.reports) {
|
|
275
|
+
const date = new Date(r.createdAt).toLocaleDateString('en-US', {
|
|
276
|
+
month: 'short',
|
|
277
|
+
day: 'numeric',
|
|
278
|
+
year: 'numeric',
|
|
279
|
+
hour: '2-digit',
|
|
280
|
+
minute: '2-digit',
|
|
281
|
+
});
|
|
282
|
+
const repo = r.repository ?? r.project;
|
|
283
|
+
const typeColor = r.type === 'plan' ? chalk.cyan : chalk.magenta;
|
|
284
|
+
console.log(` ${typeColor(r.type.padEnd(6))} ${chalk.white(repo.padEnd(40))} ${chalk.dim(r.author.padEnd(30))} ${chalk.dim(date)}`);
|
|
285
|
+
if (r.branch && r.branch !== 'main') {
|
|
286
|
+
console.log(` ${chalk.dim(`branch: ${r.branch} commit: ${r.commit ?? 'N/A'}`)}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
console.log();
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
console.error(chalk.red(`Failed to list reports: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
293
|
+
process.exit(1);
|
|
294
|
+
}
|
|
295
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -14,6 +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 { reportPlanCommand, reportAuditCommand, reportListCommand } from './commands/report.js';
|
|
17
18
|
// Read version from package.json
|
|
18
19
|
const __filename = fileURLToPath(import.meta.url);
|
|
19
20
|
const __dirname = dirname(__filename);
|
|
@@ -89,6 +90,30 @@ program
|
|
|
89
90
|
.action(async (options) => {
|
|
90
91
|
await coinsCommand(options);
|
|
91
92
|
});
|
|
93
|
+
const report = program.command('report').description('submit project reports (plans, audits)');
|
|
94
|
+
report
|
|
95
|
+
.command('plan')
|
|
96
|
+
.description('submit a plan report from a markdown file')
|
|
97
|
+
.argument('[file]', 'path to the plan file (auto-detects from .plans/ if omitted)')
|
|
98
|
+
.action(async (file) => {
|
|
99
|
+
await reportPlanCommand(file);
|
|
100
|
+
});
|
|
101
|
+
report
|
|
102
|
+
.command('audit')
|
|
103
|
+
.description('submit an audit report from a markdown file')
|
|
104
|
+
.argument('[file]', 'path to the audit file (auto-detects AUDIT.md or UX-AUDIT.md if omitted)')
|
|
105
|
+
.action(async (file) => {
|
|
106
|
+
await reportAuditCommand(file);
|
|
107
|
+
});
|
|
108
|
+
report
|
|
109
|
+
.command('list')
|
|
110
|
+
.description('list submitted reports (Team Managers only)')
|
|
111
|
+
.option('--type <type>', 'filter by type (plan, audit)')
|
|
112
|
+
.option('--repository <repo>', 'filter by repository (owner/repo)')
|
|
113
|
+
.option('--limit <n>', 'max results (default 50, max 200)')
|
|
114
|
+
.action(async (options) => {
|
|
115
|
+
await reportListCommand(options);
|
|
116
|
+
});
|
|
92
117
|
program
|
|
93
118
|
.command('version')
|
|
94
119
|
.description('show current version')
|
package/package.json
CHANGED