@browserstack/mcp-server 1.2.14 → 1.2.15-beta.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/dist/lib/percy-api/auth.d.ts +41 -0
- package/dist/lib/percy-api/auth.js +96 -0
- package/dist/lib/percy-api/cache.d.ts +28 -0
- package/dist/lib/percy-api/cache.js +48 -0
- package/dist/lib/percy-api/client.d.ts +69 -0
- package/dist/lib/percy-api/client.js +275 -0
- package/dist/lib/percy-api/errors.d.ts +15 -0
- package/dist/lib/percy-api/errors.js +52 -0
- package/dist/lib/percy-api/formatter.d.ts +16 -0
- package/dist/lib/percy-api/formatter.js +344 -0
- package/dist/lib/percy-api/percy-auth.d.ts +43 -0
- package/dist/lib/percy-api/percy-auth.js +137 -0
- package/dist/lib/percy-api/percy-error-handler.d.ts +24 -0
- package/dist/lib/percy-api/percy-error-handler.js +302 -0
- package/dist/lib/percy-api/percy-session.d.ts +42 -0
- package/dist/lib/percy-api/percy-session.js +87 -0
- package/dist/lib/percy-api/polling.d.ts +26 -0
- package/dist/lib/percy-api/polling.js +42 -0
- package/dist/lib/percy-api/types.d.ts +56 -0
- package/dist/lib/percy-api/types.js +76 -0
- package/dist/server-factory.js +4 -0
- package/dist/tools/percy-mcp/advanced/branchline-operations.d.ts +16 -0
- package/dist/tools/percy-mcp/advanced/branchline-operations.js +81 -0
- package/dist/tools/percy-mcp/advanced/manage-variants.d.ts +16 -0
- package/dist/tools/percy-mcp/advanced/manage-variants.js +155 -0
- package/dist/tools/percy-mcp/advanced/manage-visual-monitoring.d.ts +16 -0
- package/dist/tools/percy-mcp/advanced/manage-visual-monitoring.js +171 -0
- package/dist/tools/percy-mcp/auth/auth-status.d.ts +3 -0
- package/dist/tools/percy-mcp/auth/auth-status.js +131 -0
- package/dist/tools/percy-mcp/core/approve-build.d.ts +14 -0
- package/dist/tools/percy-mcp/core/approve-build.js +97 -0
- package/dist/tools/percy-mcp/core/get-build-items.d.ts +13 -0
- package/dist/tools/percy-mcp/core/get-build-items.js +65 -0
- package/dist/tools/percy-mcp/core/get-build.d.ts +10 -0
- package/dist/tools/percy-mcp/core/get-build.js +16 -0
- package/dist/tools/percy-mcp/core/get-comparison.d.ts +11 -0
- package/dist/tools/percy-mcp/core/get-comparison.js +59 -0
- package/dist/tools/percy-mcp/core/get-snapshot.d.ts +10 -0
- package/dist/tools/percy-mcp/core/get-snapshot.js +40 -0
- package/dist/tools/percy-mcp/core/list-builds.d.ts +14 -0
- package/dist/tools/percy-mcp/core/list-builds.js +45 -0
- package/dist/tools/percy-mcp/core/list-projects.d.ts +12 -0
- package/dist/tools/percy-mcp/core/list-projects.js +51 -0
- package/dist/tools/percy-mcp/creation/create-app-snapshot.d.ts +12 -0
- package/dist/tools/percy-mcp/creation/create-app-snapshot.js +29 -0
- package/dist/tools/percy-mcp/creation/create-build.d.ts +19 -0
- package/dist/tools/percy-mcp/creation/create-build.js +68 -0
- package/dist/tools/percy-mcp/creation/create-comparison.d.ts +18 -0
- package/dist/tools/percy-mcp/creation/create-comparison.js +90 -0
- package/dist/tools/percy-mcp/creation/create-snapshot.d.ts +17 -0
- package/dist/tools/percy-mcp/creation/create-snapshot.js +99 -0
- package/dist/tools/percy-mcp/creation/finalize-build.d.ts +12 -0
- package/dist/tools/percy-mcp/creation/finalize-build.js +33 -0
- package/dist/tools/percy-mcp/creation/finalize-comparison.d.ts +10 -0
- package/dist/tools/percy-mcp/creation/finalize-comparison.js +16 -0
- package/dist/tools/percy-mcp/creation/finalize-snapshot.d.ts +12 -0
- package/dist/tools/percy-mcp/creation/finalize-snapshot.js +33 -0
- package/dist/tools/percy-mcp/creation/upload-resource.d.ts +15 -0
- package/dist/tools/percy-mcp/creation/upload-resource.js +43 -0
- package/dist/tools/percy-mcp/creation/upload-tile.d.ts +11 -0
- package/dist/tools/percy-mcp/creation/upload-tile.js +53 -0
- package/dist/tools/percy-mcp/diagnostics/analyze-logs-realtime.d.ts +13 -0
- package/dist/tools/percy-mcp/diagnostics/analyze-logs-realtime.js +65 -0
- package/dist/tools/percy-mcp/diagnostics/get-build-logs.d.ts +17 -0
- package/dist/tools/percy-mcp/diagnostics/get-build-logs.js +74 -0
- package/dist/tools/percy-mcp/diagnostics/get-network-logs.d.ts +5 -0
- package/dist/tools/percy-mcp/diagnostics/get-network-logs.js +21 -0
- package/dist/tools/percy-mcp/diagnostics/get-suggestions.d.ts +7 -0
- package/dist/tools/percy-mcp/diagnostics/get-suggestions.js +24 -0
- package/dist/tools/percy-mcp/index.d.ts +36 -0
- package/dist/tools/percy-mcp/index.js +1137 -0
- package/dist/tools/percy-mcp/intelligence/get-ai-analysis.d.ts +15 -0
- package/dist/tools/percy-mcp/intelligence/get-ai-analysis.js +166 -0
- package/dist/tools/percy-mcp/intelligence/get-ai-quota.d.ts +9 -0
- package/dist/tools/percy-mcp/intelligence/get-ai-quota.js +73 -0
- package/dist/tools/percy-mcp/intelligence/get-build-summary.d.ts +11 -0
- package/dist/tools/percy-mcp/intelligence/get-build-summary.js +78 -0
- package/dist/tools/percy-mcp/intelligence/get-rca.d.ts +6 -0
- package/dist/tools/percy-mcp/intelligence/get-rca.js +153 -0
- package/dist/tools/percy-mcp/intelligence/suggest-prompt.d.ts +15 -0
- package/dist/tools/percy-mcp/intelligence/suggest-prompt.js +86 -0
- package/dist/tools/percy-mcp/intelligence/trigger-ai-recompute.d.ts +16 -0
- package/dist/tools/percy-mcp/intelligence/trigger-ai-recompute.js +64 -0
- package/dist/tools/percy-mcp/management/create-project.d.ts +14 -0
- package/dist/tools/percy-mcp/management/create-project.js +52 -0
- package/dist/tools/percy-mcp/management/get-usage-stats.d.ts +12 -0
- package/dist/tools/percy-mcp/management/get-usage-stats.js +61 -0
- package/dist/tools/percy-mcp/management/manage-browser-targets.d.ts +12 -0
- package/dist/tools/percy-mcp/management/manage-browser-targets.js +136 -0
- package/dist/tools/percy-mcp/management/manage-comments.d.ts +14 -0
- package/dist/tools/percy-mcp/management/manage-comments.js +147 -0
- package/dist/tools/percy-mcp/management/manage-ignored-regions.d.ts +18 -0
- package/dist/tools/percy-mcp/management/manage-ignored-regions.js +182 -0
- package/dist/tools/percy-mcp/management/manage-project-settings.d.ts +16 -0
- package/dist/tools/percy-mcp/management/manage-project-settings.js +97 -0
- package/dist/tools/percy-mcp/management/manage-tokens.d.ts +14 -0
- package/dist/tools/percy-mcp/management/manage-tokens.js +90 -0
- package/dist/tools/percy-mcp/management/manage-webhooks.d.ts +15 -0
- package/dist/tools/percy-mcp/management/manage-webhooks.js +180 -0
- package/dist/tools/percy-mcp/v2/auth-status.d.ts +3 -0
- package/dist/tools/percy-mcp/v2/auth-status.js +80 -0
- package/dist/tools/percy-mcp/v2/clone-build.d.ts +24 -0
- package/dist/tools/percy-mcp/v2/clone-build.js +539 -0
- package/dist/tools/percy-mcp/v2/create-app-build.d.ts +28 -0
- package/dist/tools/percy-mcp/v2/create-app-build.js +442 -0
- package/dist/tools/percy-mcp/v2/create-build.d.ts +16 -0
- package/dist/tools/percy-mcp/v2/create-build.js +601 -0
- package/dist/tools/percy-mcp/v2/create-project.d.ts +8 -0
- package/dist/tools/percy-mcp/v2/create-project.js +33 -0
- package/dist/tools/percy-mcp/v2/discover-urls.d.ts +7 -0
- package/dist/tools/percy-mcp/v2/discover-urls.js +38 -0
- package/dist/tools/percy-mcp/v2/figma-baseline.d.ts +7 -0
- package/dist/tools/percy-mcp/v2/figma-baseline.js +18 -0
- package/dist/tools/percy-mcp/v2/figma-build.d.ts +7 -0
- package/dist/tools/percy-mcp/v2/figma-build.js +39 -0
- package/dist/tools/percy-mcp/v2/figma-link.d.ts +6 -0
- package/dist/tools/percy-mcp/v2/figma-link.js +27 -0
- package/dist/tools/percy-mcp/v2/get-ai-summary.d.ts +5 -0
- package/dist/tools/percy-mcp/v2/get-ai-summary.js +109 -0
- package/dist/tools/percy-mcp/v2/get-build-detail.d.ts +22 -0
- package/dist/tools/percy-mcp/v2/get-build-detail.js +567 -0
- package/dist/tools/percy-mcp/v2/get-builds.d.ts +8 -0
- package/dist/tools/percy-mcp/v2/get-builds.js +63 -0
- package/dist/tools/percy-mcp/v2/get-comparison.d.ts +5 -0
- package/dist/tools/percy-mcp/v2/get-comparison.js +94 -0
- package/dist/tools/percy-mcp/v2/get-devices.d.ts +5 -0
- package/dist/tools/percy-mcp/v2/get-devices.js +33 -0
- package/dist/tools/percy-mcp/v2/get-insights.d.ts +7 -0
- package/dist/tools/percy-mcp/v2/get-insights.js +52 -0
- package/dist/tools/percy-mcp/v2/get-projects.d.ts +6 -0
- package/dist/tools/percy-mcp/v2/get-projects.js +41 -0
- package/dist/tools/percy-mcp/v2/get-snapshot.d.ts +5 -0
- package/dist/tools/percy-mcp/v2/get-snapshot.js +96 -0
- package/dist/tools/percy-mcp/v2/get-test-case-history.d.ts +5 -0
- package/dist/tools/percy-mcp/v2/get-test-case-history.js +20 -0
- package/dist/tools/percy-mcp/v2/get-test-cases.d.ts +6 -0
- package/dist/tools/percy-mcp/v2/get-test-cases.js +36 -0
- package/dist/tools/percy-mcp/v2/index.d.ts +35 -0
- package/dist/tools/percy-mcp/v2/index.js +544 -0
- package/dist/tools/percy-mcp/v2/list-integrations.d.ts +5 -0
- package/dist/tools/percy-mcp/v2/list-integrations.js +41 -0
- package/dist/tools/percy-mcp/v2/manage-domains.d.ts +8 -0
- package/dist/tools/percy-mcp/v2/manage-domains.js +33 -0
- package/dist/tools/percy-mcp/v2/manage-insights-email.d.ts +8 -0
- package/dist/tools/percy-mcp/v2/manage-insights-email.js +49 -0
- package/dist/tools/percy-mcp/v2/manage-usage-alerts.d.ts +10 -0
- package/dist/tools/percy-mcp/v2/manage-usage-alerts.js +43 -0
- package/dist/tools/percy-mcp/v2/migrate-integrations.d.ts +6 -0
- package/dist/tools/percy-mcp/v2/migrate-integrations.js +20 -0
- package/dist/tools/percy-mcp/v2/preview-comparison.d.ts +5 -0
- package/dist/tools/percy-mcp/v2/preview-comparison.js +17 -0
- package/dist/tools/percy-mcp/v2/search-build-items.d.ts +12 -0
- package/dist/tools/percy-mcp/v2/search-build-items.js +45 -0
- package/dist/tools/percy-mcp/workflows/auto-triage.d.ts +7 -0
- package/dist/tools/percy-mcp/workflows/auto-triage.js +82 -0
- package/dist/tools/percy-mcp/workflows/clone-build.d.ts +22 -0
- package/dist/tools/percy-mcp/workflows/clone-build.js +414 -0
- package/dist/tools/percy-mcp/workflows/create-percy-build.d.ts +32 -0
- package/dist/tools/percy-mcp/workflows/create-percy-build.js +434 -0
- package/dist/tools/percy-mcp/workflows/debug-failed-build.d.ts +5 -0
- package/dist/tools/percy-mcp/workflows/debug-failed-build.js +122 -0
- package/dist/tools/percy-mcp/workflows/diff-explain.d.ts +6 -0
- package/dist/tools/percy-mcp/workflows/diff-explain.js +147 -0
- package/dist/tools/percy-mcp/workflows/pr-visual-report.d.ts +8 -0
- package/dist/tools/percy-mcp/workflows/pr-visual-report.js +184 -0
- package/dist/tools/percy-mcp/workflows/run-tests.d.ts +17 -0
- package/dist/tools/percy-mcp/workflows/run-tests.js +107 -0
- package/dist/tools/percy-mcp/workflows/snapshot-urls.d.ts +18 -0
- package/dist/tools/percy-mcp/workflows/snapshot-urls.js +197 -0
- package/package.json +4 -3
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* percy_get_build — Unified build details tool.
|
|
3
|
+
*
|
|
4
|
+
* detail param routes to different views:
|
|
5
|
+
* - overview: status, stats, AI metrics, browsers, summary preview
|
|
6
|
+
* - ai_summary: full AI change descriptions with occurrences
|
|
7
|
+
* - changes: changed snapshots with diff ratios and bugs
|
|
8
|
+
* - rca: root cause analysis for a comparison
|
|
9
|
+
* - logs: failure diagnostics and suggestions
|
|
10
|
+
* - network: network request logs for a comparison
|
|
11
|
+
* - snapshots: all snapshots with review states
|
|
12
|
+
*/
|
|
13
|
+
import { percyGet, percyPost } from "../../../lib/percy-api/percy-auth.js";
|
|
14
|
+
import { setActiveBuild } from "../../../lib/percy-api/percy-session.js";
|
|
15
|
+
export async function percyGetBuildDetail(args, config) {
|
|
16
|
+
const detail = args.detail || "overview";
|
|
17
|
+
switch (detail) {
|
|
18
|
+
case "overview":
|
|
19
|
+
return getOverview(args.build_id, config);
|
|
20
|
+
case "ai_summary":
|
|
21
|
+
return getAiSummary(args.build_id, config);
|
|
22
|
+
case "changes":
|
|
23
|
+
return getChanges(args.build_id, config);
|
|
24
|
+
case "rca":
|
|
25
|
+
return getRca(args, config);
|
|
26
|
+
case "logs":
|
|
27
|
+
return getLogs(args.build_id, config);
|
|
28
|
+
case "network":
|
|
29
|
+
return getNetwork(args, config);
|
|
30
|
+
case "snapshots":
|
|
31
|
+
return getSnapshots(args.build_id, config);
|
|
32
|
+
default:
|
|
33
|
+
return {
|
|
34
|
+
content: [
|
|
35
|
+
{
|
|
36
|
+
type: "text",
|
|
37
|
+
text: `Unknown detail: ${detail}. Use: overview, ai_summary, changes, rca, logs, network, snapshots.`,
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
isError: true,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// ── Helper: parse build response ────────────────────────────────────────────
|
|
45
|
+
function parseBuild(response) {
|
|
46
|
+
const attrs = response?.data?.attributes || {};
|
|
47
|
+
const ai = attrs["ai-details"] || {};
|
|
48
|
+
const included = response?.included || [];
|
|
49
|
+
const rels = response?.data?.relationships || {};
|
|
50
|
+
// Parse browsers from unique-browsers-across-snapshots (more detailed)
|
|
51
|
+
const uniqueBrowsers = (attrs["unique-browsers-across-snapshots"] || []).map((b) => {
|
|
52
|
+
const bf = b.browser_family || {};
|
|
53
|
+
const os = b.operating_system || {};
|
|
54
|
+
const dp = b.device_pool || {};
|
|
55
|
+
return `${bf.name || "?"} ${b.version || ""} on ${os.name || "?"} ${os.version || ""} ${dp.name || ""}`.trim();
|
|
56
|
+
});
|
|
57
|
+
// Parse build summary
|
|
58
|
+
let summaryItems = [];
|
|
59
|
+
const summaryObj = included.find((i) => i.type === "build-summaries");
|
|
60
|
+
if (summaryObj?.attributes?.summary) {
|
|
61
|
+
const raw = summaryObj.attributes.summary;
|
|
62
|
+
try {
|
|
63
|
+
summaryItems = typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
64
|
+
if (!Array.isArray(summaryItems))
|
|
65
|
+
summaryItems = [];
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
summaryItems = [];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Parse commit
|
|
72
|
+
const commitObj = included.find((i) => i.type === "commits");
|
|
73
|
+
const commit = commitObj?.attributes || {};
|
|
74
|
+
// Base build
|
|
75
|
+
const baseBuildId = rels["base-build"]?.data?.id;
|
|
76
|
+
const hasAiData = (ai["total-comparisons-with-ai"] ?? 0) > 0 ||
|
|
77
|
+
(ai["total-potential-bugs"] ?? 0) > 0 ||
|
|
78
|
+
(ai["total-ai-visual-diffs"] ?? 0) > 0 ||
|
|
79
|
+
ai["summary-status"] === "ok";
|
|
80
|
+
return {
|
|
81
|
+
attrs,
|
|
82
|
+
ai,
|
|
83
|
+
included,
|
|
84
|
+
uniqueBrowsers,
|
|
85
|
+
summaryItems,
|
|
86
|
+
commit,
|
|
87
|
+
baseBuildId,
|
|
88
|
+
hasAiData,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
// ── Overview ────────────────────────────────────────────────────────────────
|
|
92
|
+
async function getOverview(buildId, config) {
|
|
93
|
+
const response = await percyGet(`/builds/${buildId}`, config, {
|
|
94
|
+
include: "build-summary,browsers,commit",
|
|
95
|
+
});
|
|
96
|
+
const { attrs, ai, uniqueBrowsers, summaryItems, commit, baseBuildId, hasAiData, } = parseBuild(response);
|
|
97
|
+
const buildNum = attrs["build-number"] || buildId;
|
|
98
|
+
const webUrl = attrs["web-url"] || "";
|
|
99
|
+
// Store in session
|
|
100
|
+
setActiveBuild({
|
|
101
|
+
id: buildId,
|
|
102
|
+
number: buildNum?.toString(),
|
|
103
|
+
url: webUrl,
|
|
104
|
+
branch: attrs.branch,
|
|
105
|
+
});
|
|
106
|
+
let output = `## Percy Build #${buildNum} (ID: ${buildId})\n\n`;
|
|
107
|
+
// Status table
|
|
108
|
+
output += `| Field | Value |\n|---|---|\n`;
|
|
109
|
+
output += `| **Build ID** | ${buildId} |\n`;
|
|
110
|
+
output += `| **Build #** | ${buildNum} |\n`;
|
|
111
|
+
output += `| **State** | ${attrs.state || "?"} |\n`;
|
|
112
|
+
output += `| **Branch** | ${attrs.branch || "?"} |\n`;
|
|
113
|
+
output += `| **Review** | ${attrs["review-state"] || "—"} (${attrs["review-state-reason"] || ""}) |\n`;
|
|
114
|
+
output += `| **Type** | ${attrs.type || "?"} |\n`;
|
|
115
|
+
if (webUrl)
|
|
116
|
+
output += `| **Build URL** | ${webUrl} |\n`;
|
|
117
|
+
if (commit.sha)
|
|
118
|
+
output += `| **Commit** | ${commit.sha?.slice(0, 8)} — ${commit.message || "no message"} |\n`;
|
|
119
|
+
if (commit["author-name"])
|
|
120
|
+
output += `| **Author** | ${commit["author-name"]} |\n`;
|
|
121
|
+
if (baseBuildId)
|
|
122
|
+
output += `| **Base build** | #${baseBuildId} |\n`;
|
|
123
|
+
// Stats
|
|
124
|
+
output += `\n### Stats\n\n`;
|
|
125
|
+
output += `| Metric | Value |\n|---|---|\n`;
|
|
126
|
+
output += `| Snapshots | ${attrs["total-snapshots"] ?? "?"} |\n`;
|
|
127
|
+
output += `| Comparisons | ${attrs["total-comparisons"] ?? "?"} |\n`;
|
|
128
|
+
output += `| With diffs | ${attrs["total-comparisons-diff"] ?? "—"} |\n`;
|
|
129
|
+
output += `| Unreviewed | ${attrs["total-snapshots-unreviewed"] ?? "—"} |\n`;
|
|
130
|
+
output += `| Failed | ${attrs["failed-snapshots-count"] ?? 0} |\n`;
|
|
131
|
+
output += `| Comments | ${attrs["total-open-comments"] ?? 0} |\n`;
|
|
132
|
+
output += `| Issues | ${attrs["total-open-issues"] ?? 0} |\n`;
|
|
133
|
+
// AI metrics
|
|
134
|
+
if (hasAiData) {
|
|
135
|
+
output += `\n### AI Analysis\n\n`;
|
|
136
|
+
output += `| Metric | Value |\n|---|---|\n`;
|
|
137
|
+
output += `| Potential bugs | **${ai["total-potential-bugs"] ?? 0}** |\n`;
|
|
138
|
+
output += `| AI visual diffs | ${ai["total-ai-visual-diffs"] ?? 0} |\n`;
|
|
139
|
+
output += `| Diffs reduced | ${ai["total-diffs-reduced-capped"] ?? 0} filtered |\n`;
|
|
140
|
+
output += `| Comparisons analyzed | ${ai["total-comparisons-with-ai"] ?? 0} |\n`;
|
|
141
|
+
output += `| Jobs | ${ai["all-ai-jobs-completed"] ? "completed" : "in progress"} |\n`;
|
|
142
|
+
}
|
|
143
|
+
// Browsers
|
|
144
|
+
if (uniqueBrowsers.length > 0) {
|
|
145
|
+
output += `\n### Browsers (${uniqueBrowsers.length})\n\n`;
|
|
146
|
+
uniqueBrowsers.forEach((b) => {
|
|
147
|
+
output += `- ${b}\n`;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
// AI Summary preview
|
|
151
|
+
if (summaryItems.length > 0) {
|
|
152
|
+
output += `\n### AI Summary (${summaryItems.length} changes)\n\n`;
|
|
153
|
+
summaryItems.slice(0, 3).forEach((item) => {
|
|
154
|
+
output += `- **${item.title}** (${item.occurrences} occurrences)\n`;
|
|
155
|
+
});
|
|
156
|
+
if (summaryItems.length > 3) {
|
|
157
|
+
output += `- ... and ${summaryItems.length - 3} more\n`;
|
|
158
|
+
}
|
|
159
|
+
output += `\nUse \`detail "ai_summary"\` for full details.\n`;
|
|
160
|
+
}
|
|
161
|
+
// Failure info
|
|
162
|
+
if (attrs["failure-reason"]) {
|
|
163
|
+
output += `\n### Failure\n\n`;
|
|
164
|
+
output += `**Reason:** ${attrs["failure-reason"]}\n`;
|
|
165
|
+
if (attrs["failure-details"])
|
|
166
|
+
output += `**Details:** ${attrs["failure-details"]}\n`;
|
|
167
|
+
const buckets = attrs["error-buckets"];
|
|
168
|
+
if (Array.isArray(buckets) && buckets.length > 0) {
|
|
169
|
+
output += `\n**Error categories:**\n`;
|
|
170
|
+
buckets.forEach((b) => {
|
|
171
|
+
output += `- ${b.bucket || b.name || "?"}: ${b.count ?? "?"} snapshot(s)\n`;
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// Timing
|
|
176
|
+
if (attrs["created-at"]) {
|
|
177
|
+
output += `\n### Timing\n\n`;
|
|
178
|
+
output += `| | |\n|---|---|\n`;
|
|
179
|
+
output += `| Created | ${attrs["created-at"]} |\n`;
|
|
180
|
+
if (attrs["finished-at"])
|
|
181
|
+
output += `| Finished | ${attrs["finished-at"]} |\n`;
|
|
182
|
+
if (attrs["percy-processing-duration"])
|
|
183
|
+
output += `| Processing | ${attrs["percy-processing-duration"]}s |\n`;
|
|
184
|
+
if (attrs["build-processing-duration"])
|
|
185
|
+
output += `| Total | ${attrs["build-processing-duration"]}s |\n`;
|
|
186
|
+
}
|
|
187
|
+
// URL
|
|
188
|
+
if (attrs["web-url"])
|
|
189
|
+
output += `\n**View:** ${attrs["web-url"]}\n`;
|
|
190
|
+
// Available details
|
|
191
|
+
output += `\n### More Details\n\n`;
|
|
192
|
+
output += `| Command | Shows |\n|---|---|\n`;
|
|
193
|
+
output += `| \`detail "ai_summary"\` | Full AI change descriptions with occurrences |\n`;
|
|
194
|
+
output += `| \`detail "changes"\` | Changed snapshots with diff ratios |\n`;
|
|
195
|
+
output += `| \`detail "snapshots"\` | All snapshots with review states |\n`;
|
|
196
|
+
output += `| \`detail "logs"\` | Failure diagnostics and suggestions |\n`;
|
|
197
|
+
output += `| \`detail "rca"\` | Root cause analysis (needs comparison_id) |\n`;
|
|
198
|
+
output += `| \`detail "network"\` | Network logs (needs comparison_id) |\n`;
|
|
199
|
+
return { content: [{ type: "text", text: output }] };
|
|
200
|
+
}
|
|
201
|
+
// ── AI Summary ──────────────────────────────────────────────────────────────
|
|
202
|
+
async function getAiSummary(buildId, config) {
|
|
203
|
+
const response = await percyGet(`/builds/${buildId}`, config, {
|
|
204
|
+
include: "build-summary",
|
|
205
|
+
});
|
|
206
|
+
const { attrs, ai, summaryItems, hasAiData } = parseBuild(response);
|
|
207
|
+
const buildNum = attrs["build-number"] || buildId;
|
|
208
|
+
let output = `## Build #${buildNum} — AI Summary\n\n`;
|
|
209
|
+
if (!hasAiData) {
|
|
210
|
+
output += `No AI analysis data found for this build.\n`;
|
|
211
|
+
return { content: [{ type: "text", text: output }] };
|
|
212
|
+
}
|
|
213
|
+
// AI stats
|
|
214
|
+
output += `**${ai["total-potential-bugs"] ?? 0} potential bugs** · **${ai["total-ai-visual-diffs"] ?? 0} AI visual diffs** · **${ai["total-diffs-reduced-capped"] ?? 0} diffs filtered**\n\n`;
|
|
215
|
+
output += `${ai["total-comparisons-with-ai"] ?? 0} of ${attrs["total-comparisons"] ?? "?"} comparisons analyzed by AI.\n\n`;
|
|
216
|
+
// Summary items with full detail
|
|
217
|
+
if (summaryItems.length > 0) {
|
|
218
|
+
output += `### Changes (${summaryItems.length})\n\n`;
|
|
219
|
+
summaryItems.forEach((item, i) => {
|
|
220
|
+
output += `#### ${i + 1}. ${item.title}\n\n`;
|
|
221
|
+
output += `**Occurrences:** ${item.occurrences}\n`;
|
|
222
|
+
const snaps = item.snapshots || [];
|
|
223
|
+
if (snaps.length > 0) {
|
|
224
|
+
output += `**Affected snapshots:** ${snaps.length}\n`;
|
|
225
|
+
const totalComps = snaps.reduce((sum, s) => sum + (s.comparisons?.length || 0), 0);
|
|
226
|
+
output += `**Affected comparisons:** ${totalComps}\n`;
|
|
227
|
+
// Show snapshot IDs and comparison details
|
|
228
|
+
output += `\n| Snapshot | Comparisons | Dimensions |\n|---|---|---|\n`;
|
|
229
|
+
snaps.slice(0, 5).forEach((s) => {
|
|
230
|
+
const comps = s.comparisons || [];
|
|
231
|
+
const dims = comps
|
|
232
|
+
.map((c) => `${c.width || "?"}×${c.height || "?"}`)
|
|
233
|
+
.join(", ");
|
|
234
|
+
output += `| ${s.snapshot_id} | ${comps.length} | ${dims} |\n`;
|
|
235
|
+
});
|
|
236
|
+
if (snaps.length > 5) {
|
|
237
|
+
output += `| ... | +${snaps.length - 5} more | |\n`;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
output += "\n";
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
output += `AI analysis complete but no summary items generated.\n`;
|
|
245
|
+
if (ai["summary-status"] && ai["summary-status"] !== "ok") {
|
|
246
|
+
output += `Summary status: ${ai["summary-status"]}`;
|
|
247
|
+
if (ai["summary-reason"])
|
|
248
|
+
output += ` — ${ai["summary-reason"]}`;
|
|
249
|
+
output += "\n";
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return { content: [{ type: "text", text: output }] };
|
|
253
|
+
}
|
|
254
|
+
// ── Changes ─────────────────────────────────────────────────────────────────
|
|
255
|
+
async function getChanges(buildId, config) {
|
|
256
|
+
const response = await percyGet("/build-items", config, {
|
|
257
|
+
"filter[build-id]": buildId,
|
|
258
|
+
"filter[category]": "changed",
|
|
259
|
+
"page[limit]": "30",
|
|
260
|
+
});
|
|
261
|
+
const items = response?.data || [];
|
|
262
|
+
if (!items.length) {
|
|
263
|
+
return {
|
|
264
|
+
content: [
|
|
265
|
+
{
|
|
266
|
+
type: "text",
|
|
267
|
+
text: `## Build #${buildId} — No Changes\n\nAll snapshots match the baseline.`,
|
|
268
|
+
},
|
|
269
|
+
],
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
let output = `## Build #${buildId} — Changed Snapshots (${items.length})\n\n`;
|
|
273
|
+
output += `| # | Snapshot | Display Name | Diff | Bugs | Review | Comparisons |\n|---|---|---|---|---|---|---|\n`;
|
|
274
|
+
items.forEach((item, i) => {
|
|
275
|
+
const a = item.attributes || item;
|
|
276
|
+
const name = a["cover-snapshot-name"] || a.coverSnapshotName || "?";
|
|
277
|
+
const displayName = a["cover-snapshot-display-name"] || a.coverSnapshotDisplayName || "";
|
|
278
|
+
const diff = (a["max-diff-ratio"] ?? a.maxDiffRatio) != null
|
|
279
|
+
? ((a["max-diff-ratio"] ?? a.maxDiffRatio) * 100).toFixed(1) + "%"
|
|
280
|
+
: "—";
|
|
281
|
+
const bugs = a["max-bug-total-potential-bugs"] ?? a.maxBugTotalPotentialBugs ?? 0;
|
|
282
|
+
const review = a["review-state"] || a.reviewState || "?";
|
|
283
|
+
const count = a["item-count"] || a.itemCount || 1;
|
|
284
|
+
output += `| ${i + 1} | ${name} | ${displayName || "—"} | ${diff} | ${bugs} | ${review} | ${count} |\n`;
|
|
285
|
+
});
|
|
286
|
+
output += `\nUse \`percy_get_snapshot\` with a snapshot ID from above for full details.\n`;
|
|
287
|
+
return { content: [{ type: "text", text: output }] };
|
|
288
|
+
}
|
|
289
|
+
// ── RCA ─────────────────────────────────────────────────────────────────────
|
|
290
|
+
async function getRca(args, config) {
|
|
291
|
+
if (!args.comparison_id) {
|
|
292
|
+
return {
|
|
293
|
+
content: [
|
|
294
|
+
{
|
|
295
|
+
type: "text",
|
|
296
|
+
text: `RCA requires a comparison_id.\n\nFind one with:\n\`Use percy_get_build with build_id "${args.build_id}" and detail "changes"\`\nThen: \`Use percy_get_snapshot with snapshot_id "..."\``,
|
|
297
|
+
},
|
|
298
|
+
],
|
|
299
|
+
isError: true,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
let rcaData;
|
|
303
|
+
try {
|
|
304
|
+
rcaData = await percyGet("/rca", config, {
|
|
305
|
+
comparison_id: args.comparison_id,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
try {
|
|
310
|
+
await percyPost("/rca", config, {
|
|
311
|
+
data: {
|
|
312
|
+
attributes: {
|
|
313
|
+
"comparison-id": args.comparison_id,
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
return {
|
|
318
|
+
content: [
|
|
319
|
+
{
|
|
320
|
+
type: "text",
|
|
321
|
+
text: `## RCA Triggered\n\nStarted for comparison ${args.comparison_id}. Re-run in 30-60 seconds.`,
|
|
322
|
+
},
|
|
323
|
+
],
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
catch (e) {
|
|
327
|
+
return {
|
|
328
|
+
content: [
|
|
329
|
+
{
|
|
330
|
+
type: "text",
|
|
331
|
+
text: `RCA not available: ${e.message}\nThis comparison may not have DOM metadata.`,
|
|
332
|
+
},
|
|
333
|
+
],
|
|
334
|
+
isError: true,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const status = rcaData?.data?.attributes?.status || "unknown";
|
|
339
|
+
if (status === "pending") {
|
|
340
|
+
return {
|
|
341
|
+
content: [
|
|
342
|
+
{
|
|
343
|
+
type: "text",
|
|
344
|
+
text: `## RCA — Processing\n\nStill analyzing. Try again in 30 seconds.`,
|
|
345
|
+
},
|
|
346
|
+
],
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
if (status === "failed") {
|
|
350
|
+
return {
|
|
351
|
+
content: [
|
|
352
|
+
{
|
|
353
|
+
type: "text",
|
|
354
|
+
text: `## RCA — Failed\n\nAnalysis failed. Missing DOM metadata.`,
|
|
355
|
+
},
|
|
356
|
+
],
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
let output = `## Root Cause Analysis — Comparison ${args.comparison_id}\n\n`;
|
|
360
|
+
const diffNodes = rcaData?.data?.attributes?.["diff-nodes"] || {};
|
|
361
|
+
const common = diffNodes.common_diffs || [];
|
|
362
|
+
const removed = diffNodes.extra_base || [];
|
|
363
|
+
const added = diffNodes.extra_head || [];
|
|
364
|
+
if (common.length > 0) {
|
|
365
|
+
output += `### Changed (${common.length})\n\n`;
|
|
366
|
+
output += `| # | Element | XPath | Diff Type |\n|---|---|---|---|\n`;
|
|
367
|
+
common.slice(0, 20).forEach((diff, i) => {
|
|
368
|
+
const head = diff.head || {};
|
|
369
|
+
const tag = head.tagName || "?";
|
|
370
|
+
const xpath = (head.xpath || "").slice(0, 60);
|
|
371
|
+
const dt = head.diff_type === 1
|
|
372
|
+
? "change"
|
|
373
|
+
: head.diff_type === 2
|
|
374
|
+
? "ignored"
|
|
375
|
+
: "?";
|
|
376
|
+
output += `| ${i + 1} | ${tag} | \`${xpath}\` | ${dt} |\n`;
|
|
377
|
+
});
|
|
378
|
+
output += "\n";
|
|
379
|
+
}
|
|
380
|
+
if (removed.length > 0) {
|
|
381
|
+
output += `### Removed (${removed.length})\n\n`;
|
|
382
|
+
removed.slice(0, 10).forEach((n) => {
|
|
383
|
+
const d = n.node_detail || n;
|
|
384
|
+
output += `- ${d.tagName || "element"}`;
|
|
385
|
+
if (d.xpath)
|
|
386
|
+
output += ` — \`${d.xpath.slice(0, 60)}\``;
|
|
387
|
+
output += "\n";
|
|
388
|
+
});
|
|
389
|
+
output += "\n";
|
|
390
|
+
}
|
|
391
|
+
if (added.length > 0) {
|
|
392
|
+
output += `### Added (${added.length})\n\n`;
|
|
393
|
+
added.slice(0, 10).forEach((n) => {
|
|
394
|
+
const d = n.node_detail || n;
|
|
395
|
+
output += `- ${d.tagName || "element"}`;
|
|
396
|
+
if (d.xpath)
|
|
397
|
+
output += ` — \`${d.xpath.slice(0, 60)}\``;
|
|
398
|
+
output += "\n";
|
|
399
|
+
});
|
|
400
|
+
output += "\n";
|
|
401
|
+
}
|
|
402
|
+
if (!common.length && !removed.length && !added.length) {
|
|
403
|
+
output += `No DOM differences found.\n`;
|
|
404
|
+
}
|
|
405
|
+
return { content: [{ type: "text", text: output }] };
|
|
406
|
+
}
|
|
407
|
+
// ── Logs ────────────────────────────────────────────────────────────────────
|
|
408
|
+
async function getLogs(buildId, config) {
|
|
409
|
+
let output = `## Build #${buildId} — Diagnostics\n\n`;
|
|
410
|
+
// Build info
|
|
411
|
+
try {
|
|
412
|
+
const buildResponse = await percyGet(`/builds/${buildId}`, config);
|
|
413
|
+
const attrs = buildResponse?.data?.attributes || {};
|
|
414
|
+
if (attrs["failure-reason"]) {
|
|
415
|
+
output += `### Failure\n\n`;
|
|
416
|
+
output += `**Reason:** ${attrs["failure-reason"]}\n`;
|
|
417
|
+
if (attrs["failure-details"])
|
|
418
|
+
output += `**Details:** ${attrs["failure-details"]}\n`;
|
|
419
|
+
const buckets = attrs["error-buckets"];
|
|
420
|
+
if (Array.isArray(buckets) && buckets.length > 0) {
|
|
421
|
+
output += `\n**Error categories:**\n`;
|
|
422
|
+
output += `| Category | Snapshots |\n|---|---|\n`;
|
|
423
|
+
buckets.forEach((b) => {
|
|
424
|
+
output += `| ${b.bucket || b.name || "?"} | ${b.count ?? "?"} |\n`;
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
output += "\n";
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
output += `Build state: **${attrs.state || "?"}** — no failure recorded.\n\n`;
|
|
431
|
+
}
|
|
432
|
+
// Failed snapshots
|
|
433
|
+
if ((attrs["failed-snapshots-count"] ?? 0) > 0) {
|
|
434
|
+
output += `### Failed Snapshots (${attrs["failed-snapshots-count"]})\n\n`;
|
|
435
|
+
try {
|
|
436
|
+
const failedResponse = await percyGet(`/builds/${buildId}/failed-snapshots`, config);
|
|
437
|
+
const failed = failedResponse?.data || [];
|
|
438
|
+
if (failed.length > 0) {
|
|
439
|
+
output += `| # | Name |\n|---|---|\n`;
|
|
440
|
+
failed.slice(0, 10).forEach((s, i) => {
|
|
441
|
+
output += `| ${i + 1} | ${s.attributes?.name || s.name || "?"} |\n`;
|
|
442
|
+
});
|
|
443
|
+
output += "\n";
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
catch {
|
|
447
|
+
output += `Could not fetch failed snapshot details.\n\n`;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
output += `Could not fetch build info.\n\n`;
|
|
453
|
+
}
|
|
454
|
+
// Suggestions
|
|
455
|
+
try {
|
|
456
|
+
const sugResponse = await percyGet("/suggestions", config, {
|
|
457
|
+
build_id: buildId,
|
|
458
|
+
});
|
|
459
|
+
const suggestions = sugResponse?.data || [];
|
|
460
|
+
if (Array.isArray(suggestions) && suggestions.length > 0) {
|
|
461
|
+
output += `### Suggestions (${suggestions.length})\n\n`;
|
|
462
|
+
suggestions.forEach((s, i) => {
|
|
463
|
+
const a = s.attributes || s;
|
|
464
|
+
output += `${i + 1}. **${a["bucket-display-name"] || a.bucket || "Issue"}**\n`;
|
|
465
|
+
if (a["reason-message"])
|
|
466
|
+
output += ` ${a["reason-message"]}\n`;
|
|
467
|
+
const steps = a.suggestion || [];
|
|
468
|
+
if (Array.isArray(steps)) {
|
|
469
|
+
steps.forEach((step) => {
|
|
470
|
+
output += ` - ${step}\n`;
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
if (a["reference-doc-link"])
|
|
474
|
+
output += ` [Docs](${a["reference-doc-link"]})\n`;
|
|
475
|
+
output += "\n";
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
catch {
|
|
480
|
+
/* suggestions endpoint may not exist */
|
|
481
|
+
}
|
|
482
|
+
return { content: [{ type: "text", text: output }] };
|
|
483
|
+
}
|
|
484
|
+
// ── Network ─────────────────────────────────────────────────────────────────
|
|
485
|
+
async function getNetwork(args, config) {
|
|
486
|
+
if (!args.comparison_id) {
|
|
487
|
+
return {
|
|
488
|
+
content: [
|
|
489
|
+
{
|
|
490
|
+
type: "text",
|
|
491
|
+
text: `Network logs require comparison_id.\n\nFind one with:\n\`Use percy_get_snapshot with snapshot_id "..."\``,
|
|
492
|
+
},
|
|
493
|
+
],
|
|
494
|
+
isError: true,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
const response = await percyGet("/network-logs", config, {
|
|
498
|
+
comparison_id: args.comparison_id,
|
|
499
|
+
});
|
|
500
|
+
const logs = response?.data || response || {};
|
|
501
|
+
const entries = Array.isArray(logs) ? logs : Object.values(logs);
|
|
502
|
+
if (!entries.length) {
|
|
503
|
+
return {
|
|
504
|
+
content: [
|
|
505
|
+
{
|
|
506
|
+
type: "text",
|
|
507
|
+
text: `No network logs for comparison ${args.comparison_id}.`,
|
|
508
|
+
},
|
|
509
|
+
],
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
let output = `## Network Logs — Comparison ${args.comparison_id}\n\n`;
|
|
513
|
+
output += `| # | URL | Base | Head | Type | Issue |\n|---|---|---|---|---|---|\n`;
|
|
514
|
+
entries.slice(0, 30).forEach((entry, i) => {
|
|
515
|
+
const url = entry.file || entry.domain || entry.url || "?";
|
|
516
|
+
const base = entry["base-status"] || entry.baseStatus || "—";
|
|
517
|
+
const head = entry["head-status"] || entry.headStatus || "—";
|
|
518
|
+
const type = entry.mimetype || entry.type || "—";
|
|
519
|
+
const summary = entry["status-summary"] || entry.statusSummary || "";
|
|
520
|
+
output += `| ${i + 1} | ${url} | ${base} | ${head} | ${type} | ${summary} |\n`;
|
|
521
|
+
});
|
|
522
|
+
return { content: [{ type: "text", text: output }] };
|
|
523
|
+
}
|
|
524
|
+
// ── Snapshots ───────────────────────────────────────────────────────────────
|
|
525
|
+
async function getSnapshots(buildId, config) {
|
|
526
|
+
const response = await percyGet("/build-items", config, {
|
|
527
|
+
"filter[build-id]": buildId,
|
|
528
|
+
"page[limit]": "30",
|
|
529
|
+
});
|
|
530
|
+
const items = response?.data || [];
|
|
531
|
+
if (!items.length) {
|
|
532
|
+
return {
|
|
533
|
+
content: [
|
|
534
|
+
{
|
|
535
|
+
type: "text",
|
|
536
|
+
text: `No snapshots found for build ${buildId}.`,
|
|
537
|
+
},
|
|
538
|
+
],
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
// Count totals
|
|
542
|
+
let totalItems = 0;
|
|
543
|
+
items.forEach((item) => {
|
|
544
|
+
totalItems += item.attributes?.["item-count"] || item.itemCount || 1;
|
|
545
|
+
});
|
|
546
|
+
let output = `## Build #${buildId} — Snapshots\n\n`;
|
|
547
|
+
output += `**Groups:** ${items.length} | **Total snapshots:** ${totalItems}\n\n`;
|
|
548
|
+
output += `| # | Name | Display | Diff | Bugs | Review | Items | Snapshot IDs |\n|---|---|---|---|---|---|---|---|\n`;
|
|
549
|
+
items.forEach((item, i) => {
|
|
550
|
+
const a = item.attributes || item;
|
|
551
|
+
const name = a["cover-snapshot-name"] || a.coverSnapshotName || "?";
|
|
552
|
+
const display = a["cover-snapshot-display-name"] || a.coverSnapshotDisplayName || "—";
|
|
553
|
+
const diff = (a["max-diff-ratio"] ?? a.maxDiffRatio) != null
|
|
554
|
+
? ((a["max-diff-ratio"] ?? a.maxDiffRatio) * 100).toFixed(1) + "%"
|
|
555
|
+
: "—";
|
|
556
|
+
const bugs = a["max-bug-total-potential-bugs"] ?? a.maxBugTotalPotentialBugs ?? "—";
|
|
557
|
+
const review = a["review-state"] || a.reviewState || "?";
|
|
558
|
+
const count = a["item-count"] || a.itemCount || 1;
|
|
559
|
+
const snapIds = (a["snapshot-ids"] || a.snapshotIds || [])
|
|
560
|
+
.slice(0, 3)
|
|
561
|
+
.join(", ");
|
|
562
|
+
const more = (a["snapshot-ids"] || a.snapshotIds || []).length > 3 ? "..." : "";
|
|
563
|
+
output += `| ${i + 1} | ${name} | ${display} | ${diff} | ${bugs} | ${review} | ${count} | ${snapIds}${more} |\n`;
|
|
564
|
+
});
|
|
565
|
+
output += `\nUse \`percy_get_snapshot\` with a snapshot ID for full comparison details.\n`;
|
|
566
|
+
return { content: [{ type: "text", text: output }] };
|
|
567
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { BrowserStackConfig } from "../../../lib/types.js";
|
|
2
|
+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
export declare function percyGetBuildsV2(args: {
|
|
4
|
+
project_slug?: string;
|
|
5
|
+
branch?: string;
|
|
6
|
+
state?: string;
|
|
7
|
+
limit?: number;
|
|
8
|
+
}, config: BrowserStackConfig): Promise<CallToolResult>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { percyGet } from "../../../lib/percy-api/percy-auth.js";
|
|
2
|
+
import { setActiveBuild } from "../../../lib/percy-api/percy-session.js";
|
|
3
|
+
export async function percyGetBuildsV2(args, config) {
|
|
4
|
+
let path = "/builds";
|
|
5
|
+
const params = {};
|
|
6
|
+
if (args.project_slug) {
|
|
7
|
+
path = `/projects/${args.project_slug}/builds`;
|
|
8
|
+
}
|
|
9
|
+
if (args.branch)
|
|
10
|
+
params["filter[branch]"] = args.branch;
|
|
11
|
+
if (args.state)
|
|
12
|
+
params["filter[state]"] = args.state;
|
|
13
|
+
params["page[limit]"] = String(args.limit || 10);
|
|
14
|
+
const response = await percyGet(path, config, params);
|
|
15
|
+
const builds = response?.data || [];
|
|
16
|
+
if (builds.length === 0) {
|
|
17
|
+
return {
|
|
18
|
+
content: [
|
|
19
|
+
{
|
|
20
|
+
type: "text",
|
|
21
|
+
text: "No builds found. Use `percy_get_projects` to find project slugs, then filter with `project_slug`.",
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
let output = `## Percy Builds (${builds.length})\n\n`;
|
|
27
|
+
output += `| # | Build ID | Build # | Branch | State | Review | Snapshots | Diffs | URL |\n`;
|
|
28
|
+
output += `|---|---|---|---|---|---|---|---|---|\n`;
|
|
29
|
+
builds.forEach((b, i) => {
|
|
30
|
+
const attrs = b.attributes || {};
|
|
31
|
+
const num = attrs["build-number"] || "—";
|
|
32
|
+
const branch = attrs.branch || "?";
|
|
33
|
+
const state = attrs.state || "?";
|
|
34
|
+
const review = attrs["review-state"] || "—";
|
|
35
|
+
const snaps = attrs["total-snapshots"] ?? "?";
|
|
36
|
+
const diffs = attrs["total-comparisons-diff"] ?? "—";
|
|
37
|
+
const webUrl = attrs["web-url"] || "";
|
|
38
|
+
const urlShort = webUrl ? `[View](${webUrl})` : "—";
|
|
39
|
+
output += `| ${i + 1} | ${b.id} | #${num} | ${branch} | ${state} | ${review} | ${snaps} | ${diffs} | ${urlShort} |\n`;
|
|
40
|
+
});
|
|
41
|
+
// Set the most recent build as active
|
|
42
|
+
const latest = builds[0];
|
|
43
|
+
if (latest) {
|
|
44
|
+
const latestAttrs = latest.attributes || {};
|
|
45
|
+
setActiveBuild({
|
|
46
|
+
id: latest.id,
|
|
47
|
+
number: latestAttrs["build-number"]?.toString(),
|
|
48
|
+
url: latestAttrs["web-url"],
|
|
49
|
+
branch: latestAttrs.branch,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
// Quick access to latest build
|
|
53
|
+
output += `\n### Latest Build: #${latest.attributes?.["build-number"] || latest.id} (ID: ${latest.id})\n\n`;
|
|
54
|
+
if (latest.attributes?.["web-url"]) {
|
|
55
|
+
output += `**URL:** ${latest.attributes["web-url"]}\n\n`;
|
|
56
|
+
}
|
|
57
|
+
output += `### Drill Down\n\n`;
|
|
58
|
+
output += `- \`percy_get_build\` with build_id "${latest.id}" — Full overview\n`;
|
|
59
|
+
output += `- \`percy_get_build\` with build_id "${latest.id}" and detail "snapshots" — All snapshots\n`;
|
|
60
|
+
output += `- \`percy_get_build\` with build_id "${latest.id}" and detail "ai_summary" — AI analysis\n`;
|
|
61
|
+
output += `- \`percy_search_builds\` with build_id "${latest.id}" and category "changed" — Only diffs\n`;
|
|
62
|
+
return { content: [{ type: "text", text: output }] };
|
|
63
|
+
}
|