@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.
Files changed (170) hide show
  1. package/dist/lib/percy-api/auth.d.ts +41 -0
  2. package/dist/lib/percy-api/auth.js +96 -0
  3. package/dist/lib/percy-api/cache.d.ts +28 -0
  4. package/dist/lib/percy-api/cache.js +48 -0
  5. package/dist/lib/percy-api/client.d.ts +69 -0
  6. package/dist/lib/percy-api/client.js +275 -0
  7. package/dist/lib/percy-api/errors.d.ts +15 -0
  8. package/dist/lib/percy-api/errors.js +52 -0
  9. package/dist/lib/percy-api/formatter.d.ts +16 -0
  10. package/dist/lib/percy-api/formatter.js +344 -0
  11. package/dist/lib/percy-api/percy-auth.d.ts +43 -0
  12. package/dist/lib/percy-api/percy-auth.js +137 -0
  13. package/dist/lib/percy-api/percy-error-handler.d.ts +24 -0
  14. package/dist/lib/percy-api/percy-error-handler.js +302 -0
  15. package/dist/lib/percy-api/percy-session.d.ts +42 -0
  16. package/dist/lib/percy-api/percy-session.js +87 -0
  17. package/dist/lib/percy-api/polling.d.ts +26 -0
  18. package/dist/lib/percy-api/polling.js +42 -0
  19. package/dist/lib/percy-api/types.d.ts +56 -0
  20. package/dist/lib/percy-api/types.js +76 -0
  21. package/dist/server-factory.js +4 -0
  22. package/dist/tools/percy-mcp/advanced/branchline-operations.d.ts +16 -0
  23. package/dist/tools/percy-mcp/advanced/branchline-operations.js +81 -0
  24. package/dist/tools/percy-mcp/advanced/manage-variants.d.ts +16 -0
  25. package/dist/tools/percy-mcp/advanced/manage-variants.js +155 -0
  26. package/dist/tools/percy-mcp/advanced/manage-visual-monitoring.d.ts +16 -0
  27. package/dist/tools/percy-mcp/advanced/manage-visual-monitoring.js +171 -0
  28. package/dist/tools/percy-mcp/auth/auth-status.d.ts +3 -0
  29. package/dist/tools/percy-mcp/auth/auth-status.js +131 -0
  30. package/dist/tools/percy-mcp/core/approve-build.d.ts +14 -0
  31. package/dist/tools/percy-mcp/core/approve-build.js +97 -0
  32. package/dist/tools/percy-mcp/core/get-build-items.d.ts +13 -0
  33. package/dist/tools/percy-mcp/core/get-build-items.js +65 -0
  34. package/dist/tools/percy-mcp/core/get-build.d.ts +10 -0
  35. package/dist/tools/percy-mcp/core/get-build.js +16 -0
  36. package/dist/tools/percy-mcp/core/get-comparison.d.ts +11 -0
  37. package/dist/tools/percy-mcp/core/get-comparison.js +59 -0
  38. package/dist/tools/percy-mcp/core/get-snapshot.d.ts +10 -0
  39. package/dist/tools/percy-mcp/core/get-snapshot.js +40 -0
  40. package/dist/tools/percy-mcp/core/list-builds.d.ts +14 -0
  41. package/dist/tools/percy-mcp/core/list-builds.js +45 -0
  42. package/dist/tools/percy-mcp/core/list-projects.d.ts +12 -0
  43. package/dist/tools/percy-mcp/core/list-projects.js +51 -0
  44. package/dist/tools/percy-mcp/creation/create-app-snapshot.d.ts +12 -0
  45. package/dist/tools/percy-mcp/creation/create-app-snapshot.js +29 -0
  46. package/dist/tools/percy-mcp/creation/create-build.d.ts +19 -0
  47. package/dist/tools/percy-mcp/creation/create-build.js +68 -0
  48. package/dist/tools/percy-mcp/creation/create-comparison.d.ts +18 -0
  49. package/dist/tools/percy-mcp/creation/create-comparison.js +90 -0
  50. package/dist/tools/percy-mcp/creation/create-snapshot.d.ts +17 -0
  51. package/dist/tools/percy-mcp/creation/create-snapshot.js +99 -0
  52. package/dist/tools/percy-mcp/creation/finalize-build.d.ts +12 -0
  53. package/dist/tools/percy-mcp/creation/finalize-build.js +33 -0
  54. package/dist/tools/percy-mcp/creation/finalize-comparison.d.ts +10 -0
  55. package/dist/tools/percy-mcp/creation/finalize-comparison.js +16 -0
  56. package/dist/tools/percy-mcp/creation/finalize-snapshot.d.ts +12 -0
  57. package/dist/tools/percy-mcp/creation/finalize-snapshot.js +33 -0
  58. package/dist/tools/percy-mcp/creation/upload-resource.d.ts +15 -0
  59. package/dist/tools/percy-mcp/creation/upload-resource.js +43 -0
  60. package/dist/tools/percy-mcp/creation/upload-tile.d.ts +11 -0
  61. package/dist/tools/percy-mcp/creation/upload-tile.js +53 -0
  62. package/dist/tools/percy-mcp/diagnostics/analyze-logs-realtime.d.ts +13 -0
  63. package/dist/tools/percy-mcp/diagnostics/analyze-logs-realtime.js +65 -0
  64. package/dist/tools/percy-mcp/diagnostics/get-build-logs.d.ts +17 -0
  65. package/dist/tools/percy-mcp/diagnostics/get-build-logs.js +74 -0
  66. package/dist/tools/percy-mcp/diagnostics/get-network-logs.d.ts +5 -0
  67. package/dist/tools/percy-mcp/diagnostics/get-network-logs.js +21 -0
  68. package/dist/tools/percy-mcp/diagnostics/get-suggestions.d.ts +7 -0
  69. package/dist/tools/percy-mcp/diagnostics/get-suggestions.js +24 -0
  70. package/dist/tools/percy-mcp/index.d.ts +36 -0
  71. package/dist/tools/percy-mcp/index.js +1137 -0
  72. package/dist/tools/percy-mcp/intelligence/get-ai-analysis.d.ts +15 -0
  73. package/dist/tools/percy-mcp/intelligence/get-ai-analysis.js +166 -0
  74. package/dist/tools/percy-mcp/intelligence/get-ai-quota.d.ts +9 -0
  75. package/dist/tools/percy-mcp/intelligence/get-ai-quota.js +73 -0
  76. package/dist/tools/percy-mcp/intelligence/get-build-summary.d.ts +11 -0
  77. package/dist/tools/percy-mcp/intelligence/get-build-summary.js +78 -0
  78. package/dist/tools/percy-mcp/intelligence/get-rca.d.ts +6 -0
  79. package/dist/tools/percy-mcp/intelligence/get-rca.js +153 -0
  80. package/dist/tools/percy-mcp/intelligence/suggest-prompt.d.ts +15 -0
  81. package/dist/tools/percy-mcp/intelligence/suggest-prompt.js +86 -0
  82. package/dist/tools/percy-mcp/intelligence/trigger-ai-recompute.d.ts +16 -0
  83. package/dist/tools/percy-mcp/intelligence/trigger-ai-recompute.js +64 -0
  84. package/dist/tools/percy-mcp/management/create-project.d.ts +14 -0
  85. package/dist/tools/percy-mcp/management/create-project.js +52 -0
  86. package/dist/tools/percy-mcp/management/get-usage-stats.d.ts +12 -0
  87. package/dist/tools/percy-mcp/management/get-usage-stats.js +61 -0
  88. package/dist/tools/percy-mcp/management/manage-browser-targets.d.ts +12 -0
  89. package/dist/tools/percy-mcp/management/manage-browser-targets.js +136 -0
  90. package/dist/tools/percy-mcp/management/manage-comments.d.ts +14 -0
  91. package/dist/tools/percy-mcp/management/manage-comments.js +147 -0
  92. package/dist/tools/percy-mcp/management/manage-ignored-regions.d.ts +18 -0
  93. package/dist/tools/percy-mcp/management/manage-ignored-regions.js +182 -0
  94. package/dist/tools/percy-mcp/management/manage-project-settings.d.ts +16 -0
  95. package/dist/tools/percy-mcp/management/manage-project-settings.js +97 -0
  96. package/dist/tools/percy-mcp/management/manage-tokens.d.ts +14 -0
  97. package/dist/tools/percy-mcp/management/manage-tokens.js +90 -0
  98. package/dist/tools/percy-mcp/management/manage-webhooks.d.ts +15 -0
  99. package/dist/tools/percy-mcp/management/manage-webhooks.js +180 -0
  100. package/dist/tools/percy-mcp/v2/auth-status.d.ts +3 -0
  101. package/dist/tools/percy-mcp/v2/auth-status.js +80 -0
  102. package/dist/tools/percy-mcp/v2/clone-build.d.ts +24 -0
  103. package/dist/tools/percy-mcp/v2/clone-build.js +539 -0
  104. package/dist/tools/percy-mcp/v2/create-app-build.d.ts +28 -0
  105. package/dist/tools/percy-mcp/v2/create-app-build.js +442 -0
  106. package/dist/tools/percy-mcp/v2/create-build.d.ts +16 -0
  107. package/dist/tools/percy-mcp/v2/create-build.js +601 -0
  108. package/dist/tools/percy-mcp/v2/create-project.d.ts +8 -0
  109. package/dist/tools/percy-mcp/v2/create-project.js +33 -0
  110. package/dist/tools/percy-mcp/v2/discover-urls.d.ts +7 -0
  111. package/dist/tools/percy-mcp/v2/discover-urls.js +38 -0
  112. package/dist/tools/percy-mcp/v2/figma-baseline.d.ts +7 -0
  113. package/dist/tools/percy-mcp/v2/figma-baseline.js +18 -0
  114. package/dist/tools/percy-mcp/v2/figma-build.d.ts +7 -0
  115. package/dist/tools/percy-mcp/v2/figma-build.js +39 -0
  116. package/dist/tools/percy-mcp/v2/figma-link.d.ts +6 -0
  117. package/dist/tools/percy-mcp/v2/figma-link.js +27 -0
  118. package/dist/tools/percy-mcp/v2/get-ai-summary.d.ts +5 -0
  119. package/dist/tools/percy-mcp/v2/get-ai-summary.js +109 -0
  120. package/dist/tools/percy-mcp/v2/get-build-detail.d.ts +22 -0
  121. package/dist/tools/percy-mcp/v2/get-build-detail.js +567 -0
  122. package/dist/tools/percy-mcp/v2/get-builds.d.ts +8 -0
  123. package/dist/tools/percy-mcp/v2/get-builds.js +63 -0
  124. package/dist/tools/percy-mcp/v2/get-comparison.d.ts +5 -0
  125. package/dist/tools/percy-mcp/v2/get-comparison.js +94 -0
  126. package/dist/tools/percy-mcp/v2/get-devices.d.ts +5 -0
  127. package/dist/tools/percy-mcp/v2/get-devices.js +33 -0
  128. package/dist/tools/percy-mcp/v2/get-insights.d.ts +7 -0
  129. package/dist/tools/percy-mcp/v2/get-insights.js +52 -0
  130. package/dist/tools/percy-mcp/v2/get-projects.d.ts +6 -0
  131. package/dist/tools/percy-mcp/v2/get-projects.js +41 -0
  132. package/dist/tools/percy-mcp/v2/get-snapshot.d.ts +5 -0
  133. package/dist/tools/percy-mcp/v2/get-snapshot.js +96 -0
  134. package/dist/tools/percy-mcp/v2/get-test-case-history.d.ts +5 -0
  135. package/dist/tools/percy-mcp/v2/get-test-case-history.js +20 -0
  136. package/dist/tools/percy-mcp/v2/get-test-cases.d.ts +6 -0
  137. package/dist/tools/percy-mcp/v2/get-test-cases.js +36 -0
  138. package/dist/tools/percy-mcp/v2/index.d.ts +35 -0
  139. package/dist/tools/percy-mcp/v2/index.js +544 -0
  140. package/dist/tools/percy-mcp/v2/list-integrations.d.ts +5 -0
  141. package/dist/tools/percy-mcp/v2/list-integrations.js +41 -0
  142. package/dist/tools/percy-mcp/v2/manage-domains.d.ts +8 -0
  143. package/dist/tools/percy-mcp/v2/manage-domains.js +33 -0
  144. package/dist/tools/percy-mcp/v2/manage-insights-email.d.ts +8 -0
  145. package/dist/tools/percy-mcp/v2/manage-insights-email.js +49 -0
  146. package/dist/tools/percy-mcp/v2/manage-usage-alerts.d.ts +10 -0
  147. package/dist/tools/percy-mcp/v2/manage-usage-alerts.js +43 -0
  148. package/dist/tools/percy-mcp/v2/migrate-integrations.d.ts +6 -0
  149. package/dist/tools/percy-mcp/v2/migrate-integrations.js +20 -0
  150. package/dist/tools/percy-mcp/v2/preview-comparison.d.ts +5 -0
  151. package/dist/tools/percy-mcp/v2/preview-comparison.js +17 -0
  152. package/dist/tools/percy-mcp/v2/search-build-items.d.ts +12 -0
  153. package/dist/tools/percy-mcp/v2/search-build-items.js +45 -0
  154. package/dist/tools/percy-mcp/workflows/auto-triage.d.ts +7 -0
  155. package/dist/tools/percy-mcp/workflows/auto-triage.js +82 -0
  156. package/dist/tools/percy-mcp/workflows/clone-build.d.ts +22 -0
  157. package/dist/tools/percy-mcp/workflows/clone-build.js +414 -0
  158. package/dist/tools/percy-mcp/workflows/create-percy-build.d.ts +32 -0
  159. package/dist/tools/percy-mcp/workflows/create-percy-build.js +434 -0
  160. package/dist/tools/percy-mcp/workflows/debug-failed-build.d.ts +5 -0
  161. package/dist/tools/percy-mcp/workflows/debug-failed-build.js +122 -0
  162. package/dist/tools/percy-mcp/workflows/diff-explain.d.ts +6 -0
  163. package/dist/tools/percy-mcp/workflows/diff-explain.js +147 -0
  164. package/dist/tools/percy-mcp/workflows/pr-visual-report.d.ts +8 -0
  165. package/dist/tools/percy-mcp/workflows/pr-visual-report.js +184 -0
  166. package/dist/tools/percy-mcp/workflows/run-tests.d.ts +17 -0
  167. package/dist/tools/percy-mcp/workflows/run-tests.js +107 -0
  168. package/dist/tools/percy-mcp/workflows/snapshot-urls.d.ts +18 -0
  169. package/dist/tools/percy-mcp/workflows/snapshot-urls.js +197 -0
  170. package/package.json +4 -3
@@ -0,0 +1,24 @@
1
+ /**
2
+ * percy_clone_build — Clone a build into a different project with 100% parity.
3
+ *
4
+ * Preserves: snapshot names, all widths, all browsers, all device info.
5
+ *
6
+ * Two modes (auto-selected):
7
+ * 1. URL Replay (`percy snapshot`): URL-named snapshots → full DOM re-render
8
+ * 2. Screenshot Clone (direct API): Named snapshots → downloads all screenshots,
9
+ * re-uploads via tile API. Each snapshot keeps its exact name with all
10
+ * width/browser/device comparisons intact.
11
+ *
12
+ * Screenshot clone uses tile-based API which requires app-type project.
13
+ * If target project is web-type, creates with same name as app-type.
14
+ */
15
+ import { BrowserStackConfig } from "../../../lib/types.js";
16
+ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
17
+ interface CloneBuildArgs {
18
+ source_build_id: string;
19
+ target_project_name: string;
20
+ target_token?: string;
21
+ branch?: string;
22
+ }
23
+ export declare function percyCloneBuildV2(args: CloneBuildArgs, config: BrowserStackConfig): Promise<CallToolResult>;
24
+ export {};
@@ -0,0 +1,539 @@
1
+ /**
2
+ * percy_clone_build — Clone a build into a different project with 100% parity.
3
+ *
4
+ * Preserves: snapshot names, all widths, all browsers, all device info.
5
+ *
6
+ * Two modes (auto-selected):
7
+ * 1. URL Replay (`percy snapshot`): URL-named snapshots → full DOM re-render
8
+ * 2. Screenshot Clone (direct API): Named snapshots → downloads all screenshots,
9
+ * re-uploads via tile API. Each snapshot keeps its exact name with all
10
+ * width/browser/device comparisons intact.
11
+ *
12
+ * Screenshot clone uses tile-based API which requires app-type project.
13
+ * If target project is web-type, creates with same name as app-type.
14
+ */
15
+ import { percyGet, percyTokenPost, getOrCreateProjectToken, getPercyAuthHeaders, } from "../../../lib/percy-api/percy-auth.js";
16
+ import { createHash } from "crypto";
17
+ import { execFile, spawn } from "child_process";
18
+ import { promisify } from "util";
19
+ import { writeFile, unlink, mkdtemp } from "fs/promises";
20
+ import { join } from "path";
21
+ import { tmpdir } from "os";
22
+ const execFileAsync = promisify(execFile);
23
+ async function getGitBranch() {
24
+ try {
25
+ return ((await execFileAsync("git", ["branch", "--show-current"])).stdout.trim() || "main");
26
+ }
27
+ catch {
28
+ return "main";
29
+ }
30
+ }
31
+ /** Strip base64 padding — Percy requires strict base64 (RFC 4648 §4.1) */
32
+ function toStrictBase64(buffer) {
33
+ return buffer.toString("base64").replace(/=+$/, "");
34
+ }
35
+ /** Small delay to avoid rate limiting */
36
+ function delay(ms) {
37
+ return new Promise((r) => setTimeout(r, ms));
38
+ }
39
+ export async function percyCloneBuildV2(args, config) {
40
+ const branch = args.branch || (await getGitBranch());
41
+ let output = `## Percy Build Clone\n\n`;
42
+ output += `**Source:** Build #${args.source_build_id}\n`;
43
+ output += `**Target:** ${args.target_project_name}\n`;
44
+ output += `**Branch:** ${branch}\n\n`;
45
+ // ── Step 1: Read source build ─────────────────────────────────────────
46
+ let sourceBuild;
47
+ try {
48
+ sourceBuild = await percyGet(`/builds/${args.source_build_id}`, config);
49
+ }
50
+ catch (e) {
51
+ return {
52
+ content: [
53
+ { type: "text", text: `Failed to read source build: ${e.message}` },
54
+ ],
55
+ isError: true,
56
+ };
57
+ }
58
+ const sourceAttrs = sourceBuild?.data?.attributes || {};
59
+ const buildType = sourceAttrs.type || "web";
60
+ output += `Source: **${sourceAttrs.state}** — ${sourceAttrs["total-snapshots"]} snapshots, type: ${buildType}\n\n`;
61
+ // ── Step 2: Get ALL snapshot details with ALL comparisons ─────────────
62
+ const headers = getPercyAuthHeaders(config);
63
+ const baseUrl = "https://percy.io/api/v1";
64
+ let allSnapshotIds = [];
65
+ try {
66
+ const items = await percyGet("/build-items", config, {
67
+ "filter[build-id]": args.source_build_id,
68
+ "page[limit]": "50",
69
+ });
70
+ const itemList = items?.data || [];
71
+ for (const item of itemList) {
72
+ const a = item.attributes || item;
73
+ const ids = a["snapshot-ids"] || a.snapshotIds || [];
74
+ if (ids.length > 0) {
75
+ allSnapshotIds.push(...ids.map(String));
76
+ }
77
+ else if (a["cover-snapshot-id"] || a.coverSnapshotId) {
78
+ allSnapshotIds.push(String(a["cover-snapshot-id"] || a.coverSnapshotId));
79
+ }
80
+ }
81
+ allSnapshotIds = [...new Set(allSnapshotIds)];
82
+ }
83
+ catch (e) {
84
+ return {
85
+ content: [
86
+ { type: "text", text: `Failed to read snapshots: ${e.message}` },
87
+ ],
88
+ isError: true,
89
+ };
90
+ }
91
+ output += `Found **${allSnapshotIds.length}** snapshots.\n\n`;
92
+ if (allSnapshotIds.length === 0) {
93
+ output += `Nothing to clone.\n`;
94
+ return { content: [{ type: "text", text: output }] };
95
+ }
96
+ // Read snapshot metadata with ALL comparison details (all browsers, all widths)
97
+ const snapshots = [];
98
+ let totalComps = 0;
99
+ for (const snapId of allSnapshotIds) {
100
+ try {
101
+ const snapResponse = await fetch(`${baseUrl}/snapshots/${snapId}?include=comparisons.head-screenshot.image,comparisons.comparison-tag`, { headers });
102
+ if (!snapResponse.ok)
103
+ continue;
104
+ const snapJson = await snapResponse.json();
105
+ const sa = snapJson.data?.attributes || {};
106
+ const included = snapJson.included || [];
107
+ const byTypeId = new Map();
108
+ for (const item of included) {
109
+ byTypeId.set(`${item.type}:${item.id}`, item);
110
+ }
111
+ const compRefs = snapJson.data?.relationships?.comparisons?.data || [];
112
+ const comparisons = [];
113
+ const widthSet = new Set();
114
+ for (const ref of compRefs) {
115
+ const comp = byTypeId.get(`comparisons:${ref.id}`);
116
+ if (!comp)
117
+ continue;
118
+ const width = comp.attributes?.width || 1280;
119
+ widthSet.add(width);
120
+ let imageUrl = null;
121
+ let height = 800;
122
+ const hsRef = comp.relationships?.["head-screenshot"]?.data;
123
+ if (hsRef) {
124
+ const ss = byTypeId.get(`screenshots:${hsRef.id}`);
125
+ const imgRef = ss?.relationships?.image?.data;
126
+ if (imgRef) {
127
+ const img = byTypeId.get(`images:${imgRef.id}`);
128
+ if (img) {
129
+ imageUrl = img.attributes?.url || null;
130
+ height = img.attributes?.height || 800;
131
+ }
132
+ }
133
+ }
134
+ // Extract REAL device/browser info from comparison tag
135
+ const tagRef = comp.relationships?.["comparison-tag"]?.data;
136
+ let tagName = "Chrome";
137
+ let osName = "";
138
+ let osVersion = "";
139
+ let browserName = "Chrome";
140
+ let browserVersion = "";
141
+ let orientation = "portrait";
142
+ if (tagRef) {
143
+ const tag = byTypeId.get(`comparison-tags:${tagRef.id}`);
144
+ if (tag?.attributes) {
145
+ const ta = tag.attributes;
146
+ tagName = ta.name || tagName;
147
+ osName = ta["os-name"] || osName;
148
+ osVersion = ta["os-version"] || osVersion;
149
+ browserName = ta["browser-name"] || browserName;
150
+ browserVersion = ta["browser-version"] || browserVersion;
151
+ orientation = ta.orientation || orientation;
152
+ }
153
+ }
154
+ comparisons.push({
155
+ width,
156
+ height,
157
+ tagName,
158
+ osName,
159
+ osVersion,
160
+ browserName,
161
+ browserVersion,
162
+ orientation,
163
+ imageUrl,
164
+ });
165
+ }
166
+ totalComps += comparisons.length;
167
+ snapshots.push({
168
+ id: snapId,
169
+ name: sa.name || `Snapshot ${snapId}`,
170
+ displayName: sa["display-name"] || sa.name || "",
171
+ widths: [...widthSet].sort(),
172
+ enableJavascript: sa["enable-javascript"] || false,
173
+ testCase: sa["test-case-name"] || null,
174
+ comparisons,
175
+ });
176
+ }
177
+ catch {
178
+ /* skip failed snapshots */
179
+ }
180
+ }
181
+ // Show what we found
182
+ const browsers = new Set();
183
+ const widths = new Set();
184
+ for (const snap of snapshots) {
185
+ for (const c of snap.comparisons) {
186
+ browsers.add(c.browserName || c.tagName);
187
+ widths.add(c.width);
188
+ }
189
+ }
190
+ output += `Read **${snapshots.length}** snapshots, **${totalComps}** comparisons\n`;
191
+ output += `Browsers: ${[...browsers].join(", ")}\n`;
192
+ output += `Widths: ${[...widths].sort((a, b) => a - b).join(", ")}px\n\n`;
193
+ // ── Step 3: Ensure Percy CLI is available (auto-install if missing) ───
194
+ let hasCli = false;
195
+ try {
196
+ await execFileAsync("npx", ["@percy/cli", "--version"]);
197
+ hasCli = true;
198
+ }
199
+ catch {
200
+ output += `Installing Percy CLI...\n`;
201
+ try {
202
+ await execFileAsync("npm", ["install", "-g", "@percy/cli"], {
203
+ timeout: 60000,
204
+ });
205
+ hasCli = true;
206
+ output += `Percy CLI installed.\n\n`;
207
+ }
208
+ catch {
209
+ hasCli = false;
210
+ }
211
+ }
212
+ // ── Step 4: Determine clone mode ──────────────────────────────────────
213
+ const hasUrlNames = snapshots.some((s) => s.name.startsWith("http://") || s.name.startsWith("https://"));
214
+ if (hasCli && hasUrlNames) {
215
+ // URL Replay: Percy CLI re-snapshots with full DOM/CSS/JS
216
+ let targetToken;
217
+ if (args.target_token) {
218
+ targetToken = args.target_token;
219
+ }
220
+ else {
221
+ try {
222
+ targetToken = await getOrCreateProjectToken(args.target_project_name, config);
223
+ }
224
+ catch (e) {
225
+ return {
226
+ content: [
227
+ { type: "text", text: `Failed to get target token: ${e.message}` },
228
+ ],
229
+ isError: true,
230
+ };
231
+ }
232
+ }
233
+ return await replayWithPercyCli(output, snapshots, targetToken, branch, args.target_project_name);
234
+ }
235
+ // ── Screenshot Clone via tile API ─────────────────────────────────────
236
+ // Tile-based API preserves: exact snapshot names, all widths, all browsers.
237
+ // MUST be app-type project — web projects require DOM resources for snapshots
238
+ // and reject tile-based uploads. This is an immutable Percy API constraint.
239
+ let targetToken;
240
+ const actualProjectName = args.target_project_name;
241
+ if (args.target_token) {
242
+ targetToken = args.target_token;
243
+ }
244
+ else {
245
+ // Always request app type — tiles only work on app/automate/generic projects.
246
+ // If project exists as web-type, this will fail and we retry with a suffix.
247
+ try {
248
+ targetToken = await getOrCreateProjectToken(args.target_project_name, config, "app");
249
+ }
250
+ catch {
251
+ // Project exists as web-type — can't use tiles on it.
252
+ // Create companion project with same name + "-screenshots" as app type.
253
+ const altName = `${args.target_project_name}-screenshots`;
254
+ output += `"${args.target_project_name}" is web-type (needs DOM resources).\n`;
255
+ output += `Creating **${altName}** (app-type) for screenshot clone.\n\n`;
256
+ try {
257
+ targetToken = await getOrCreateProjectToken(altName, config, "app");
258
+ }
259
+ catch (e) {
260
+ return {
261
+ content: [
262
+ { type: "text", text: `Failed to create project: ${e.message}` },
263
+ ],
264
+ isError: true,
265
+ };
266
+ }
267
+ }
268
+ }
269
+ return await cloneViaApi(output, snapshots, targetToken, branch, actualProjectName, totalComps);
270
+ }
271
+ // ── URL Replay (Percy CLI) ──────────────────────────────────────────────────
272
+ async function replayWithPercyCli(output, snapshots, token, branch, projectName) {
273
+ output += `### Mode: URL Replay (Percy CLI)\n\n`;
274
+ output += `**Project:** ${projectName}\n`;
275
+ output += `Percy CLI will re-snapshot each page with full resource discovery.\n\n`;
276
+ let yamlContent = "";
277
+ const uniqueNames = new Set();
278
+ for (const snap of snapshots) {
279
+ if (uniqueNames.has(snap.name))
280
+ continue;
281
+ uniqueNames.add(snap.name);
282
+ const name = snap.displayName || snap.name;
283
+ const widths = snap.widths.length > 0 ? snap.widths : [1280];
284
+ yamlContent += `- name: "${name}"\n`;
285
+ if (snap.name.startsWith("http://") || snap.name.startsWith("https://")) {
286
+ yamlContent += ` url: ${snap.name}\n`;
287
+ }
288
+ else {
289
+ yamlContent += ` url: "UNKNOWN"\n`;
290
+ }
291
+ yamlContent += ` waitForTimeout: 3000\n`;
292
+ if (snap.enableJavascript) {
293
+ yamlContent += ` enableJavaScript: true\n`;
294
+ }
295
+ if (snap.testCase) {
296
+ yamlContent += ` testCase: "${snap.testCase}"\n`;
297
+ }
298
+ yamlContent += ` widths:\n`;
299
+ widths.forEach((w) => {
300
+ yamlContent += ` - ${w}\n`;
301
+ });
302
+ }
303
+ const hasUrls = snapshots.some((s) => s.name.startsWith("http://") || s.name.startsWith("https://"));
304
+ if (!hasUrls) {
305
+ output += `Snapshots don't have URL names. Use \`percy_create_build\` with URLs.\n`;
306
+ return { content: [{ type: "text", text: output }] };
307
+ }
308
+ const tmpDir = await mkdtemp(join(tmpdir(), "percy-clone-"));
309
+ const configPath = join(tmpDir, "snapshots.yml");
310
+ await writeFile(configPath, yamlContent);
311
+ const child = spawn("npx", ["@percy/cli", "snapshot", configPath], {
312
+ env: { ...process.env, PERCY_TOKEN: token, PERCY_BRANCH: branch },
313
+ stdio: ["ignore", "pipe", "pipe"],
314
+ detached: true,
315
+ });
316
+ let buildUrl = "";
317
+ let stdoutData = "";
318
+ child.stdout?.on("data", (d) => {
319
+ const text = d.toString();
320
+ stdoutData += text;
321
+ const match = text.match(/https:\/\/percy\.io\/[^\s]+\/builds\/\d+/);
322
+ if (match)
323
+ buildUrl = match[0];
324
+ });
325
+ child.stderr?.on("data", (d) => {
326
+ stdoutData += d.toString();
327
+ });
328
+ await new Promise((resolve) => {
329
+ const timeout = setTimeout(resolve, 30000);
330
+ child.on("close", () => {
331
+ clearTimeout(timeout);
332
+ resolve();
333
+ });
334
+ const check = setInterval(() => {
335
+ if (buildUrl) {
336
+ clearTimeout(timeout);
337
+ clearInterval(check);
338
+ resolve();
339
+ }
340
+ }, 500);
341
+ });
342
+ child.unref();
343
+ setTimeout(async () => {
344
+ try {
345
+ await unlink(configPath);
346
+ }
347
+ catch {
348
+ /* ignore */
349
+ }
350
+ }, 120000);
351
+ output += `**Replaying ${uniqueNames.size} snapshots...**\n\n`;
352
+ if (buildUrl) {
353
+ output += `**Build URL:** ${buildUrl}\n\n`;
354
+ output += `Percy CLI is re-snapshotting with full resource discovery.\n`;
355
+ output += `Results ready in 1-3 minutes.\n`;
356
+ }
357
+ else {
358
+ const percyLines = stdoutData
359
+ .split("\n")
360
+ .filter((l) => l.includes("[percy"))
361
+ .slice(0, 10);
362
+ if (percyLines.length > 0) {
363
+ output += `**Percy output:**\n\`\`\`\n${percyLines.join("\n")}\n\`\`\`\n`;
364
+ }
365
+ else {
366
+ output += `Percy is processing in background. Check dashboard.\n`;
367
+ }
368
+ }
369
+ return { content: [{ type: "text", text: output }] };
370
+ }
371
+ // ── Screenshot Clone (direct API with tiles) ────────────────────────────────
372
+ //
373
+ // Full parity clone: same snapshot names, all widths, all browsers, all devices.
374
+ //
375
+ // API flow per snapshot:
376
+ // 1. POST /builds/:id/snapshots → exact source name
377
+ // 2. For each comparison (width × browser):
378
+ // a. Download screenshot image
379
+ // b. POST /snapshots/:id/comparisons → tag (browser/device) + tile SHA
380
+ // c. POST /comparisons/:id/tiles → upload image (strict base64)
381
+ // d. POST /comparisons/:id/finalize → finalize comparison
382
+ // 3. POST /builds/:id/finalize → finalize build
383
+ //
384
+ async function cloneViaApi(output, snapshots, token, branch, projectName, totalComps) {
385
+ output += `### Mode: Screenshot Clone (full parity)\n\n`;
386
+ output += `**Project:** ${projectName}\n`;
387
+ output += `Cloning ${snapshots.length} snapshots, ${totalComps} comparisons.\n\n`;
388
+ const commitSha = createHash("sha1")
389
+ .update(Date.now().toString())
390
+ .digest("hex");
391
+ // Step 1: Create build
392
+ let buildResult;
393
+ try {
394
+ buildResult = await percyTokenPost("/builds", token, {
395
+ data: {
396
+ type: "builds",
397
+ attributes: { branch, "commit-sha": commitSha },
398
+ },
399
+ });
400
+ }
401
+ catch (e) {
402
+ output += `Failed to create build: ${e.message}\n`;
403
+ return { content: [{ type: "text", text: output }], isError: true };
404
+ }
405
+ const buildId = buildResult?.data?.id;
406
+ const buildUrl = buildResult?.data?.attributes?.["web-url"] || "";
407
+ output += `Build: **#${buildId}**`;
408
+ if (buildUrl)
409
+ output += ` — ${buildUrl}`;
410
+ output += "\n\n";
411
+ let clonedSnaps = 0;
412
+ let clonedComps = 0;
413
+ let failedComps = 0;
414
+ for (const snap of snapshots) {
415
+ const compsWithImages = snap.comparisons.filter((c) => c.imageUrl);
416
+ if (compsWithImages.length === 0) {
417
+ output += `- ${snap.name} — no screenshots, skipped\n`;
418
+ continue;
419
+ }
420
+ try {
421
+ // Step 2: Create snapshot with EXACT source name
422
+ const snapResult = await percyTokenPost(`/builds/${buildId}/snapshots`, token, {
423
+ data: {
424
+ type: "snapshots",
425
+ attributes: { name: snap.name },
426
+ },
427
+ });
428
+ const newSnapId = snapResult?.data?.id;
429
+ if (!newSnapId) {
430
+ output += `- ${snap.name} — snapshot creation failed\n`;
431
+ continue;
432
+ }
433
+ let snapCompCount = 0;
434
+ // Step 3: Create comparison for EACH width × browser combo
435
+ for (const comp of compsWithImages) {
436
+ try {
437
+ // Download screenshot
438
+ const imgResponse = await fetch(comp.imageUrl);
439
+ if (!imgResponse.ok) {
440
+ failedComps++;
441
+ continue;
442
+ }
443
+ const imgBuffer = Buffer.from(await imgResponse.arrayBuffer());
444
+ const sha = createHash("sha256").update(imgBuffer).digest("hex");
445
+ const base64 = toStrictBase64(imgBuffer);
446
+ // Create comparison: attributes (required) + relationships (tag + tiles)
447
+ const compResult = await percyTokenPost(`/snapshots/${newSnapId}/comparisons`, token, {
448
+ data: {
449
+ type: "comparisons",
450
+ attributes: {
451
+ "external-debug-url": null,
452
+ "dom-info-sha": null,
453
+ },
454
+ relationships: {
455
+ tag: {
456
+ data: {
457
+ type: "tag",
458
+ attributes: {
459
+ name: comp.tagName,
460
+ width: comp.width,
461
+ height: comp.height,
462
+ "os-name": comp.osName || "",
463
+ "os-version": comp.osVersion || "",
464
+ "browser-name": comp.browserName || "",
465
+ "browser-version": comp.browserVersion || "",
466
+ orientation: comp.orientation || "portrait",
467
+ },
468
+ },
469
+ },
470
+ tiles: {
471
+ data: [
472
+ {
473
+ type: "tiles",
474
+ attributes: {
475
+ sha,
476
+ "status-bar-height": 0,
477
+ "nav-bar-height": 0,
478
+ "header-height": 0,
479
+ "footer-height": 0,
480
+ fullscreen: false,
481
+ },
482
+ },
483
+ ],
484
+ },
485
+ },
486
+ },
487
+ });
488
+ const compId = compResult?.data?.id;
489
+ if (compId) {
490
+ // Upload tile image
491
+ await percyTokenPost(`/comparisons/${compId}/tiles`, token, {
492
+ data: {
493
+ type: "tiles",
494
+ attributes: { "base64-content": base64 },
495
+ },
496
+ });
497
+ // Finalize comparison
498
+ await percyTokenPost(`/comparisons/${compId}/finalize`, token, {});
499
+ snapCompCount++;
500
+ clonedComps++;
501
+ }
502
+ else {
503
+ failedComps++;
504
+ }
505
+ }
506
+ catch (compErr) {
507
+ failedComps++;
508
+ const msg = compErr.message?.slice(0, 100) || "unknown error";
509
+ output += ` ! ${comp.browserName} ${comp.width}px: ${msg}\n`;
510
+ }
511
+ // Rate limit protection — 500ms between comparisons (3 API calls each)
512
+ await delay(500);
513
+ }
514
+ clonedSnaps++;
515
+ output += `- **${snap.name}** — ${snapCompCount}/${compsWithImages.length} comparisons\n`;
516
+ }
517
+ catch (e) {
518
+ output += `- FAILED ${snap.name}: ${e.message}\n`;
519
+ }
520
+ // Delay between snapshots to avoid Cloudflare rate limits
521
+ await delay(1000);
522
+ }
523
+ // Finalize build
524
+ try {
525
+ await percyTokenPost(`/builds/${buildId}/finalize`, token, {});
526
+ }
527
+ catch (e) {
528
+ output += `\nFinalize failed: ${e.message}\n`;
529
+ }
530
+ // Summary
531
+ output += `\n---\n`;
532
+ output += `**Result:** ${clonedSnaps}/${snapshots.length} snapshots, ${clonedComps}/${totalComps} comparisons cloned`;
533
+ if (failedComps > 0)
534
+ output += ` (${failedComps} failed)`;
535
+ output += `\n`;
536
+ if (buildUrl)
537
+ output += `**View:** ${buildUrl}\n`;
538
+ return { content: [{ type: "text", text: output }] };
539
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * percy_create_app_build — Create an App Percy BYOS (Bring Your Own Screenshots) build.
3
+ *
4
+ * Two modes:
5
+ * 1. Sample mode (use_sample_data=true): auto-generates 3 devices × 2 screenshots
6
+ * using sharp. Zero setup — just provide a project name.
7
+ * 2. Custom mode (resources_dir): reads your own device folders with device.json + PNGs.
8
+ *
9
+ * Expected directory structure for custom mode:
10
+ * resources/
11
+ * iPhone_14_Pro/
12
+ * device.json ← { deviceName, osName, osVersion, orientation, deviceScreenSize }
13
+ * Home.png
14
+ * Settings.png
15
+ * Pixel_7/
16
+ * device.json
17
+ * Home.png
18
+ */
19
+ import { BrowserStackConfig } from "../../../lib/types.js";
20
+ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
21
+ export interface CreateAppBuildArgs {
22
+ project_name: string;
23
+ resources_dir?: string;
24
+ use_sample_data?: boolean;
25
+ branch?: string;
26
+ test_case?: string;
27
+ }
28
+ export declare function percyCreateAppBuildV2(args: CreateAppBuildArgs, config: BrowserStackConfig): Promise<CallToolResult>;