@vizzly-testing/cli 0.23.0 → 0.23.2
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/README.md +54 -586
- package/dist/api/client.js +3 -1
- package/dist/api/endpoints.js +6 -7
- package/dist/cli.js +15 -2
- package/dist/commands/finalize.js +12 -0
- package/dist/commands/preview.js +210 -28
- package/dist/commands/run.js +15 -0
- package/dist/commands/status.js +34 -8
- package/dist/commands/upload.js +13 -0
- package/dist/reporter/reporter-bundle.iife.js +19 -19
- package/dist/utils/ci-env.js +114 -16
- package/package.json +1 -2
- package/claude-plugin/.claude-plugin/README.md +0 -270
- package/claude-plugin/.claude-plugin/marketplace.json +0 -28
- package/claude-plugin/.claude-plugin/plugin.json +0 -14
- package/claude-plugin/.mcp.json +0 -12
- package/claude-plugin/CHANGELOG.md +0 -85
- package/claude-plugin/commands/setup.md +0 -137
- package/claude-plugin/commands/suggest-screenshots.md +0 -111
- package/claude-plugin/mcp/vizzly-docs-server/README.md +0 -95
- package/claude-plugin/mcp/vizzly-docs-server/docs-fetcher.js +0 -110
- package/claude-plugin/mcp/vizzly-docs-server/index.js +0 -283
- package/claude-plugin/mcp/vizzly-server/cloud-api-provider.js +0 -399
- package/claude-plugin/mcp/vizzly-server/index.js +0 -927
- package/claude-plugin/mcp/vizzly-server/local-tdd-provider.js +0 -455
- package/claude-plugin/mcp/vizzly-server/token-resolver.js +0 -185
- package/claude-plugin/skills/check-visual-tests/SKILL.md +0 -158
- package/claude-plugin/skills/debug-visual-regression/SKILL.md +0 -269
- package/docs/api-reference.md +0 -1003
- package/docs/authentication.md +0 -334
- package/docs/doctor-command.md +0 -44
- package/docs/getting-started.md +0 -131
- package/docs/internal/SDK-API.md +0 -1018
- package/docs/plugins.md +0 -557
- package/docs/tdd-mode.md +0 -594
- package/docs/test-integration.md +0 -523
- package/docs/tui-elements.md +0 -560
- package/docs/upload-command.md +0 -196
|
@@ -1,283 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Vizzly Docs MCP Server
|
|
5
|
-
* Provides Claude Code with easy access to Vizzly documentation
|
|
6
|
-
*
|
|
7
|
-
* This server fetches docs from the deployed docs.vizzly.dev site,
|
|
8
|
-
* making it easy for LLMs to navigate and retrieve documentation content.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
12
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
13
|
-
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
14
|
-
import { fetchDocsIndex, fetchDocContent, searchDocs } from './docs-fetcher.js';
|
|
15
|
-
|
|
16
|
-
class VizzlyDocsMCPServer {
|
|
17
|
-
constructor() {
|
|
18
|
-
this.server = new Server(
|
|
19
|
-
{
|
|
20
|
-
name: 'vizzly-docs',
|
|
21
|
-
version: '1.0.0'
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
capabilities: {
|
|
25
|
-
tools: {}
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
);
|
|
29
|
-
|
|
30
|
-
// Cache the index for the session
|
|
31
|
-
this.indexCache = null;
|
|
32
|
-
this.indexFetchTime = null;
|
|
33
|
-
this.CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
34
|
-
|
|
35
|
-
this.setupHandlers();
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Get the docs index (with caching)
|
|
40
|
-
*/
|
|
41
|
-
async getIndex() {
|
|
42
|
-
let now = Date.now();
|
|
43
|
-
|
|
44
|
-
if (!this.indexCache || !this.indexFetchTime || now - this.indexFetchTime > this.CACHE_TTL) {
|
|
45
|
-
this.indexCache = await fetchDocsIndex();
|
|
46
|
-
this.indexFetchTime = now;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return this.indexCache;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
setupHandlers() {
|
|
53
|
-
// List available tools
|
|
54
|
-
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
55
|
-
tools: [
|
|
56
|
-
{
|
|
57
|
-
name: 'list_docs',
|
|
58
|
-
description:
|
|
59
|
-
'List all available Vizzly documentation pages. Returns title, description, category, and URL for each doc. Optionally filter by category.',
|
|
60
|
-
inputSchema: {
|
|
61
|
-
type: 'object',
|
|
62
|
-
properties: {
|
|
63
|
-
category: {
|
|
64
|
-
type: 'string',
|
|
65
|
-
description:
|
|
66
|
-
'Optional category filter (e.g., "Integration > CLI", "Features"). Case-insensitive partial match.'
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
name: 'get_doc',
|
|
73
|
-
description:
|
|
74
|
-
'Get the full markdown content of a specific documentation page. Returns the raw MDX/markdown with frontmatter. Use the path or slug from list_docs.',
|
|
75
|
-
inputSchema: {
|
|
76
|
-
type: 'object',
|
|
77
|
-
properties: {
|
|
78
|
-
path: {
|
|
79
|
-
type: 'string',
|
|
80
|
-
description:
|
|
81
|
-
'The document path (e.g., "integration/cli/overview.mdx") or slug (e.g., "integration/cli/overview"). Get this from list_docs or search_docs.'
|
|
82
|
-
}
|
|
83
|
-
},
|
|
84
|
-
required: ['path']
|
|
85
|
-
}
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
name: 'search_docs',
|
|
89
|
-
description:
|
|
90
|
-
'Search documentation by keyword. Searches in titles and descriptions. Returns matching docs with relevance scores.',
|
|
91
|
-
inputSchema: {
|
|
92
|
-
type: 'object',
|
|
93
|
-
properties: {
|
|
94
|
-
query: {
|
|
95
|
-
type: 'string',
|
|
96
|
-
description: 'Search query (e.g., "TDD mode", "authentication", "parallel builds")'
|
|
97
|
-
},
|
|
98
|
-
limit: {
|
|
99
|
-
type: 'number',
|
|
100
|
-
description: 'Maximum number of results to return (default: 10)',
|
|
101
|
-
default: 10
|
|
102
|
-
}
|
|
103
|
-
},
|
|
104
|
-
required: ['query']
|
|
105
|
-
}
|
|
106
|
-
},
|
|
107
|
-
{
|
|
108
|
-
name: 'get_sidebar',
|
|
109
|
-
description:
|
|
110
|
-
'Get the complete sidebar navigation structure. Useful for understanding how docs are organized and finding related pages.',
|
|
111
|
-
inputSchema: {
|
|
112
|
-
type: 'object',
|
|
113
|
-
properties: {}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
]
|
|
117
|
-
}));
|
|
118
|
-
|
|
119
|
-
// Handle tool calls
|
|
120
|
-
this.server.setRequestHandler(CallToolRequestSchema, async request => {
|
|
121
|
-
try {
|
|
122
|
-
switch (request.params.name) {
|
|
123
|
-
case 'list_docs':
|
|
124
|
-
return await this.handleListDocs(request.params.arguments);
|
|
125
|
-
|
|
126
|
-
case 'get_doc':
|
|
127
|
-
return await this.handleGetDoc(request.params.arguments);
|
|
128
|
-
|
|
129
|
-
case 'search_docs':
|
|
130
|
-
return await this.handleSearchDocs(request.params.arguments);
|
|
131
|
-
|
|
132
|
-
case 'get_sidebar':
|
|
133
|
-
return await this.handleGetSidebar();
|
|
134
|
-
|
|
135
|
-
default:
|
|
136
|
-
throw new Error(`Unknown tool: ${request.params.name}`);
|
|
137
|
-
}
|
|
138
|
-
} catch (error) {
|
|
139
|
-
return {
|
|
140
|
-
content: [
|
|
141
|
-
{
|
|
142
|
-
type: 'text',
|
|
143
|
-
text: `Error: ${error.message}`
|
|
144
|
-
}
|
|
145
|
-
],
|
|
146
|
-
isError: true
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
async handleListDocs(args) {
|
|
153
|
-
let index = await this.getIndex();
|
|
154
|
-
let { category } = args || {};
|
|
155
|
-
|
|
156
|
-
let docs = index.docs;
|
|
157
|
-
|
|
158
|
-
// Filter by category if provided
|
|
159
|
-
if (category) {
|
|
160
|
-
let lowerCategory = category.toLowerCase();
|
|
161
|
-
docs = docs.filter(doc => doc.category.toLowerCase().includes(lowerCategory));
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Format response
|
|
165
|
-
let response = `# Vizzly Documentation (${docs.length} docs)\n\n`;
|
|
166
|
-
|
|
167
|
-
if (category) {
|
|
168
|
-
response += `Filtered by category: "${category}"\n\n`;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Group by category
|
|
172
|
-
let byCategory = {};
|
|
173
|
-
for (let doc of docs) {
|
|
174
|
-
if (!byCategory[doc.category]) {
|
|
175
|
-
byCategory[doc.category] = [];
|
|
176
|
-
}
|
|
177
|
-
byCategory[doc.category].push(doc);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
for (let [cat, catDocs] of Object.entries(byCategory)) {
|
|
181
|
-
response += `## ${cat}\n\n`;
|
|
182
|
-
for (let doc of catDocs) {
|
|
183
|
-
response += `- **${doc.title}**\n`;
|
|
184
|
-
response += ` - Path: \`${doc.path}\`\n`;
|
|
185
|
-
response += ` - Slug: \`${doc.slug}\`\n`;
|
|
186
|
-
if (doc.description) {
|
|
187
|
-
response += ` - ${doc.description}\n`;
|
|
188
|
-
}
|
|
189
|
-
response += ` - URL: ${doc.url}\n\n`;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
return {
|
|
194
|
-
content: [
|
|
195
|
-
{
|
|
196
|
-
type: 'text',
|
|
197
|
-
text: response
|
|
198
|
-
}
|
|
199
|
-
]
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
async handleGetDoc(args) {
|
|
204
|
-
let { path } = args;
|
|
205
|
-
|
|
206
|
-
if (!path) {
|
|
207
|
-
throw new Error('path parameter is required');
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
let content = await fetchDocContent(path);
|
|
211
|
-
|
|
212
|
-
return {
|
|
213
|
-
content: [
|
|
214
|
-
{
|
|
215
|
-
type: 'text',
|
|
216
|
-
text: content
|
|
217
|
-
}
|
|
218
|
-
]
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
async handleSearchDocs(args) {
|
|
223
|
-
let { query, limit = 10 } = args;
|
|
224
|
-
|
|
225
|
-
if (!query) {
|
|
226
|
-
throw new Error('query parameter is required');
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
let index = await this.getIndex();
|
|
230
|
-
let results = searchDocs(index.docs, query, limit);
|
|
231
|
-
|
|
232
|
-
let response = `# Search Results for "${query}"\n\n`;
|
|
233
|
-
response += `Found ${results.length} matching docs:\n\n`;
|
|
234
|
-
|
|
235
|
-
for (let result of results) {
|
|
236
|
-
response += `## ${result.doc.title}\n`;
|
|
237
|
-
response += `- **Category:** ${result.doc.category}\n`;
|
|
238
|
-
response += `- **Path:** \`${result.doc.path}\`\n`;
|
|
239
|
-
response += `- **Relevance:** ${Math.round(result.score * 100)}%\n`;
|
|
240
|
-
if (result.doc.description) {
|
|
241
|
-
response += `- **Description:** ${result.doc.description}\n`;
|
|
242
|
-
}
|
|
243
|
-
response += `- **URL:** ${result.doc.url}\n\n`;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
return {
|
|
247
|
-
content: [
|
|
248
|
-
{
|
|
249
|
-
type: 'text',
|
|
250
|
-
text: response
|
|
251
|
-
}
|
|
252
|
-
]
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
async handleGetSidebar() {
|
|
257
|
-
let index = await this.getIndex();
|
|
258
|
-
|
|
259
|
-
let response = `# Vizzly Documentation Structure\n\n`;
|
|
260
|
-
response += JSON.stringify(index.sidebar, null, 2);
|
|
261
|
-
|
|
262
|
-
return {
|
|
263
|
-
content: [
|
|
264
|
-
{
|
|
265
|
-
type: 'text',
|
|
266
|
-
text: response
|
|
267
|
-
}
|
|
268
|
-
]
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
async run() {
|
|
273
|
-
let transport = new StdioServerTransport();
|
|
274
|
-
await this.server.connect(transport);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Start the server
|
|
279
|
-
let server = new VizzlyDocsMCPServer();
|
|
280
|
-
server.run().catch(error => {
|
|
281
|
-
console.error('Server error:', error);
|
|
282
|
-
process.exit(1);
|
|
283
|
-
});
|
|
@@ -1,399 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Provider for Vizzly Cloud API integration
|
|
3
|
-
*/
|
|
4
|
-
export class CloudAPIProvider {
|
|
5
|
-
constructor() {
|
|
6
|
-
this.defaultApiUrl = process.env.VIZZLY_API_URL || 'https://app.vizzly.dev';
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Make API request to Vizzly
|
|
11
|
-
*/
|
|
12
|
-
async makeRequest(path, apiToken, apiUrl = this.defaultApiUrl) {
|
|
13
|
-
if (!apiToken) {
|
|
14
|
-
throw new Error(
|
|
15
|
-
'API token required. Set VIZZLY_TOKEN environment variable or provide via apiToken parameter.'
|
|
16
|
-
);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
let url = `${apiUrl}${path}`;
|
|
20
|
-
let response = await fetch(url, {
|
|
21
|
-
headers: {
|
|
22
|
-
Authorization: `Bearer ${apiToken}`,
|
|
23
|
-
'User-Agent': 'Vizzly-Claude-Plugin/0.1.0'
|
|
24
|
-
}
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
if (!response.ok) {
|
|
28
|
-
let error = await response.text();
|
|
29
|
-
throw new Error(`API request failed (${response.status}): ${error}`);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return response.json();
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Get build status and details
|
|
37
|
-
*/
|
|
38
|
-
async getBuildStatus(buildId, apiToken, apiUrl) {
|
|
39
|
-
if (!buildId || typeof buildId !== 'string') {
|
|
40
|
-
throw new Error('buildId is required and must be a non-empty string');
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
let data = await this.makeRequest(
|
|
44
|
-
`/api/sdk/builds/${buildId}?include=comparisons`,
|
|
45
|
-
apiToken,
|
|
46
|
-
apiUrl
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
let { build } = data;
|
|
50
|
-
|
|
51
|
-
// Calculate comparison summary
|
|
52
|
-
// Note: API returns 'result' field (not 'status'), and doesn't have 'has_diff'
|
|
53
|
-
// result can be: 'identical', 'changed', 'new', 'removed', 'error', 'missing', 'returning'
|
|
54
|
-
let comparisons = build.comparisons || [];
|
|
55
|
-
|
|
56
|
-
// Calculate basic comparison summary
|
|
57
|
-
let changedComparisons = comparisons.filter((c) => c.result === 'changed' || (c.diff_percentage && c.diff_percentage > 0));
|
|
58
|
-
|
|
59
|
-
let summary = {
|
|
60
|
-
total: comparisons.length,
|
|
61
|
-
new: comparisons.filter((c) => c.result === 'new').length,
|
|
62
|
-
changed: changedComparisons.length,
|
|
63
|
-
identical: comparisons.filter((c) => c.result === 'identical' || (c.result !== 'new' && c.result !== 'changed' && (!c.diff_percentage || c.diff_percentage === 0))).length,
|
|
64
|
-
// Approval status breakdown
|
|
65
|
-
approval: {
|
|
66
|
-
pending: comparisons.filter((c) => c.approval_status === 'pending').length,
|
|
67
|
-
approved: comparisons.filter((c) => c.approval_status === 'approved').length,
|
|
68
|
-
rejected: comparisons.filter((c) => c.approval_status === 'rejected').length,
|
|
69
|
-
autoApproved: comparisons.filter((c) => c.approval_status === 'auto_approved').length
|
|
70
|
-
},
|
|
71
|
-
// Flaky screenshot count
|
|
72
|
-
flaky: comparisons.filter((c) => c.is_flaky).length
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
// Keep minimal for token efficiency - use read_comparison_details for full metadata
|
|
76
|
-
let failedComparisons = comparisons
|
|
77
|
-
.filter((c) => c.result === 'changed' || (c.diff_percentage && c.diff_percentage > 0))
|
|
78
|
-
.map((c) => ({
|
|
79
|
-
id: c.id,
|
|
80
|
-
name: c.current_name || c.name,
|
|
81
|
-
diffPercentage: c.diff_percentage,
|
|
82
|
-
approvalStatus: c.approval_status,
|
|
83
|
-
// Include hot spot coverage % for quick triage (single number)
|
|
84
|
-
hotSpotCoverage: c.analysis_metadata?.hot_spot_coverage || null
|
|
85
|
-
}));
|
|
86
|
-
|
|
87
|
-
let newComparisons = comparisons
|
|
88
|
-
.filter((c) => c.result === 'new')
|
|
89
|
-
.map((c) => ({
|
|
90
|
-
name: c.name,
|
|
91
|
-
currentUrl: c.current_screenshot?.original_url
|
|
92
|
-
}));
|
|
93
|
-
|
|
94
|
-
return {
|
|
95
|
-
build: {
|
|
96
|
-
id: build.id,
|
|
97
|
-
name: build.name,
|
|
98
|
-
branch: build.branch,
|
|
99
|
-
status: build.status,
|
|
100
|
-
url: build.url,
|
|
101
|
-
organizationSlug: build.organizationSlug,
|
|
102
|
-
projectSlug: build.projectSlug,
|
|
103
|
-
createdAt: build.created_at,
|
|
104
|
-
// Include commit details for debugging
|
|
105
|
-
commitSha: build.commit_sha,
|
|
106
|
-
commitMessage: build.commit_message,
|
|
107
|
-
commonAncestorSha: build.common_ancestor_sha
|
|
108
|
-
},
|
|
109
|
-
summary,
|
|
110
|
-
failedComparisons,
|
|
111
|
-
newComparisons
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* List recent builds
|
|
117
|
-
*/
|
|
118
|
-
async listRecentBuilds(apiToken, options = {}) {
|
|
119
|
-
let { limit = 10, branch, apiUrl } = options;
|
|
120
|
-
|
|
121
|
-
let queryParams = new URLSearchParams({
|
|
122
|
-
limit: limit.toString()
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
if (branch) {
|
|
126
|
-
queryParams.append('branch', branch);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
let data = await this.makeRequest(`/api/sdk/builds?${queryParams}`, apiToken, apiUrl);
|
|
130
|
-
|
|
131
|
-
return {
|
|
132
|
-
builds: data.builds.map((b) => ({
|
|
133
|
-
id: b.id,
|
|
134
|
-
name: b.name,
|
|
135
|
-
branch: b.branch,
|
|
136
|
-
status: b.status,
|
|
137
|
-
environment: b.environment,
|
|
138
|
-
createdAt: b.created_at
|
|
139
|
-
})),
|
|
140
|
-
pagination: data.pagination
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Get token context (organization and project info)
|
|
146
|
-
*/
|
|
147
|
-
async getTokenContext(apiToken, apiUrl) {
|
|
148
|
-
return await this.makeRequest('/api/sdk/token/context', apiToken, apiUrl);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Get comparison details
|
|
153
|
-
*/
|
|
154
|
-
async getComparison(comparisonId, apiToken, apiUrl) {
|
|
155
|
-
if (!comparisonId || typeof comparisonId !== 'string') {
|
|
156
|
-
throw new Error('comparisonId is required and must be a non-empty string');
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
let data = await this.makeRequest(`/api/sdk/comparisons/${comparisonId}`, apiToken, apiUrl);
|
|
160
|
-
|
|
161
|
-
return data.comparison;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Search for comparisons by name across builds
|
|
166
|
-
*/
|
|
167
|
-
async searchComparisons(name, apiToken, options = {}) {
|
|
168
|
-
if (!name || typeof name !== 'string') {
|
|
169
|
-
throw new Error('name is required and must be a non-empty string');
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
let { branch, limit = 50, offset = 0, apiUrl } = options;
|
|
173
|
-
|
|
174
|
-
let queryParams = new URLSearchParams({
|
|
175
|
-
name,
|
|
176
|
-
limit: limit.toString(),
|
|
177
|
-
offset: offset.toString()
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
if (branch) {
|
|
181
|
-
queryParams.append('branch', branch);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
let data = await this.makeRequest(
|
|
185
|
-
`/api/sdk/comparisons/search?${queryParams}`,
|
|
186
|
-
apiToken,
|
|
187
|
-
apiUrl
|
|
188
|
-
);
|
|
189
|
-
|
|
190
|
-
return data;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// ==================================================================
|
|
194
|
-
// BUILD COMMENTS
|
|
195
|
-
// ==================================================================
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Create a comment on a build
|
|
199
|
-
*/
|
|
200
|
-
async createBuildComment(buildId, content, type, apiToken, apiUrl) {
|
|
201
|
-
if (!buildId || typeof buildId !== 'string') {
|
|
202
|
-
throw new Error('buildId is required and must be a non-empty string');
|
|
203
|
-
}
|
|
204
|
-
if (!content || typeof content !== 'string') {
|
|
205
|
-
throw new Error('content is required and must be a non-empty string');
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
let url = `${apiUrl || this.defaultApiUrl}/api/sdk/builds/${buildId}/comments`;
|
|
209
|
-
let response = await fetch(url, {
|
|
210
|
-
method: 'POST',
|
|
211
|
-
headers: {
|
|
212
|
-
Authorization: `Bearer ${apiToken}`,
|
|
213
|
-
'User-Agent': 'Vizzly-Claude-Plugin/0.1.0',
|
|
214
|
-
'Content-Type': 'application/json'
|
|
215
|
-
},
|
|
216
|
-
body: JSON.stringify({ content, type })
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
if (!response.ok) {
|
|
220
|
-
let error = await response.text();
|
|
221
|
-
throw new Error(`Failed to create comment (${response.status}): ${error}`);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
return response.json();
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* List comments for a build
|
|
229
|
-
*/
|
|
230
|
-
async listBuildComments(buildId, apiToken, apiUrl) {
|
|
231
|
-
if (!buildId || typeof buildId !== 'string') {
|
|
232
|
-
throw new Error('buildId is required and must be a non-empty string');
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
let data = await this.makeRequest(`/api/sdk/builds/${buildId}/comments`, apiToken, apiUrl);
|
|
236
|
-
|
|
237
|
-
// Filter out unnecessary fields from comments for MCP
|
|
238
|
-
let filterComment = (comment) => {
|
|
239
|
-
// eslint-disable-next-line no-unused-vars
|
|
240
|
-
let { profile_photo_url, email, ...filtered } = comment;
|
|
241
|
-
// Recursively filter replies if they exist
|
|
242
|
-
if (filtered.replies && Array.isArray(filtered.replies)) {
|
|
243
|
-
filtered.replies = filtered.replies.map(filterComment);
|
|
244
|
-
}
|
|
245
|
-
return filtered;
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
return {
|
|
249
|
-
...data,
|
|
250
|
-
comments: data.comments ? data.comments.map(filterComment) : []
|
|
251
|
-
};
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// ==================================================================
|
|
255
|
-
// COMPARISON APPROVALS
|
|
256
|
-
// ==================================================================
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Approve a comparison
|
|
260
|
-
*/
|
|
261
|
-
async approveComparison(comparisonId, comment, apiToken, apiUrl) {
|
|
262
|
-
if (!comparisonId || typeof comparisonId !== 'string') {
|
|
263
|
-
throw new Error('comparisonId is required and must be a non-empty string');
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
let url = `${apiUrl || this.defaultApiUrl}/api/sdk/comparisons/${comparisonId}/approve`;
|
|
267
|
-
let response = await fetch(url, {
|
|
268
|
-
method: 'POST',
|
|
269
|
-
headers: {
|
|
270
|
-
Authorization: `Bearer ${apiToken}`,
|
|
271
|
-
'User-Agent': 'Vizzly-Claude-Plugin/0.1.0',
|
|
272
|
-
'Content-Type': 'application/json'
|
|
273
|
-
},
|
|
274
|
-
body: JSON.stringify({ comment })
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
if (!response.ok) {
|
|
278
|
-
let error = await response.text();
|
|
279
|
-
throw new Error(`Failed to approve comparison (${response.status}): ${error}`);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
return response.json();
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* Reject a comparison
|
|
287
|
-
*/
|
|
288
|
-
async rejectComparison(comparisonId, reason, apiToken, apiUrl) {
|
|
289
|
-
if (!comparisonId || typeof comparisonId !== 'string') {
|
|
290
|
-
throw new Error('comparisonId is required and must be a non-empty string');
|
|
291
|
-
}
|
|
292
|
-
if (!reason || typeof reason !== 'string') {
|
|
293
|
-
throw new Error('reason is required and must be a non-empty string');
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
let url = `${apiUrl || this.defaultApiUrl}/api/sdk/comparisons/${comparisonId}/reject`;
|
|
297
|
-
let response = await fetch(url, {
|
|
298
|
-
method: 'POST',
|
|
299
|
-
headers: {
|
|
300
|
-
Authorization: `Bearer ${apiToken}`,
|
|
301
|
-
'User-Agent': 'Vizzly-Claude-Plugin/0.1.0',
|
|
302
|
-
'Content-Type': 'application/json'
|
|
303
|
-
},
|
|
304
|
-
body: JSON.stringify({ reason })
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
if (!response.ok) {
|
|
308
|
-
let error = await response.text();
|
|
309
|
-
throw new Error(`Failed to reject comparison (${response.status}): ${error}`);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
return response.json();
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* Update comparison approval status
|
|
317
|
-
*/
|
|
318
|
-
async updateComparisonApproval(comparisonId, approvalStatus, comment, apiToken, apiUrl) {
|
|
319
|
-
let url = `${apiUrl || this.defaultApiUrl}/api/sdk/comparisons/${comparisonId}/approval`;
|
|
320
|
-
let response = await fetch(url, {
|
|
321
|
-
method: 'PUT',
|
|
322
|
-
headers: {
|
|
323
|
-
Authorization: `Bearer ${apiToken}`,
|
|
324
|
-
'User-Agent': 'Vizzly-Claude-Plugin/0.1.0',
|
|
325
|
-
'Content-Type': 'application/json'
|
|
326
|
-
},
|
|
327
|
-
body: JSON.stringify({ approval_status: approvalStatus, comment })
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
if (!response.ok) {
|
|
331
|
-
let error = await response.text();
|
|
332
|
-
throw new Error(`Failed to update comparison approval (${response.status}): ${error}`);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
return response.json();
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// ==================================================================
|
|
339
|
-
// REVIEW STATUS
|
|
340
|
-
// ==================================================================
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* Get review summary for a build
|
|
344
|
-
*/
|
|
345
|
-
async getReviewSummary(buildId, apiToken, apiUrl) {
|
|
346
|
-
if (!buildId || typeof buildId !== 'string') {
|
|
347
|
-
throw new Error('buildId is required and must be a non-empty string');
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
let data = await this.makeRequest(
|
|
351
|
-
`/api/sdk/builds/${buildId}/review-summary`,
|
|
352
|
-
apiToken,
|
|
353
|
-
apiUrl
|
|
354
|
-
);
|
|
355
|
-
|
|
356
|
-
return data;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// ==================================================================
|
|
360
|
-
// TDD WORKFLOW
|
|
361
|
-
// ==================================================================
|
|
362
|
-
|
|
363
|
-
/**
|
|
364
|
-
* Download baseline screenshots from a cloud build
|
|
365
|
-
* Returns screenshot data that can be saved locally
|
|
366
|
-
*/
|
|
367
|
-
async downloadBaselines(buildId, screenshotNames, apiToken, apiUrl) {
|
|
368
|
-
if (!buildId || typeof buildId !== 'string') {
|
|
369
|
-
throw new Error('buildId is required and must be a non-empty string');
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
let data = await this.makeRequest(
|
|
373
|
-
`/api/sdk/builds/${buildId}?include=screenshots`,
|
|
374
|
-
apiToken,
|
|
375
|
-
apiUrl
|
|
376
|
-
);
|
|
377
|
-
|
|
378
|
-
let { build } = data;
|
|
379
|
-
let screenshots = build.screenshots || [];
|
|
380
|
-
|
|
381
|
-
// Filter by screenshot names if provided
|
|
382
|
-
if (screenshotNames && screenshotNames.length > 0) {
|
|
383
|
-
screenshots = screenshots.filter((s) => screenshotNames.includes(s.name));
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
return {
|
|
387
|
-
buildId: build.id,
|
|
388
|
-
buildName: build.name,
|
|
389
|
-
screenshots: screenshots.map((s) => ({
|
|
390
|
-
name: s.name,
|
|
391
|
-
url: s.original_url,
|
|
392
|
-
sha256: s.sha256,
|
|
393
|
-
width: s.viewport_width,
|
|
394
|
-
height: s.viewport_height,
|
|
395
|
-
browser: s.browser
|
|
396
|
-
}))
|
|
397
|
-
};
|
|
398
|
-
}
|
|
399
|
-
}
|