devlens-mcp 0.4.2 → 0.5.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 +211 -35
- package/RELEASE_NOTES.md +3 -7
- package/dist/bin/cli.js +41 -14
- package/dist/bin/cli.js.map +1 -1
- package/dist/bin/setup.d.ts +14 -0
- package/dist/bin/setup.d.ts.map +1 -0
- package/dist/bin/setup.js +272 -0
- package/dist/bin/setup.js.map +1 -0
- package/dist/src/figma/figma-api.d.ts +108 -0
- package/dist/src/figma/figma-api.d.ts.map +1 -0
- package/dist/src/figma/figma-api.js +284 -0
- package/dist/src/figma/figma-api.js.map +1 -0
- package/dist/src/figma/figma-cache.d.ts +57 -0
- package/dist/src/figma/figma-cache.d.ts.map +1 -0
- package/dist/src/figma/figma-cache.js +266 -0
- package/dist/src/figma/figma-cache.js.map +1 -0
- package/dist/src/figma/figma-client.d.ts +62 -0
- package/dist/src/figma/figma-client.d.ts.map +1 -0
- package/dist/src/figma/figma-client.js +224 -0
- package/dist/src/figma/figma-client.js.map +1 -0
- package/dist/src/figma/figma-rate-limiter.d.ts +46 -0
- package/dist/src/figma/figma-rate-limiter.d.ts.map +1 -0
- package/dist/src/figma/figma-rate-limiter.js +153 -0
- package/dist/src/figma/figma-rate-limiter.js.map +1 -0
- package/dist/src/figma/figma-retry.d.ts +19 -0
- package/dist/src/figma/figma-retry.d.ts.map +1 -0
- package/dist/src/figma/figma-retry.js +57 -0
- package/dist/src/figma/figma-retry.js.map +1 -0
- package/dist/src/figma/figma-structure.d.ts +1 -1
- package/dist/src/figma/figma-structure.d.ts.map +1 -1
- package/dist/src/figma/figma-structure.js +11 -1
- package/dist/src/figma/figma-structure.js.map +1 -1
- package/dist/src/figma/figma-types.d.ts +54 -0
- package/dist/src/figma/figma-types.d.ts.map +1 -0
- package/dist/src/figma/figma-types.js +33 -0
- package/dist/src/figma/figma-types.js.map +1 -0
- package/dist/src/figma/url-parser.d.ts +40 -0
- package/dist/src/figma/url-parser.d.ts.map +1 -0
- package/dist/src/figma/url-parser.js +107 -0
- package/dist/src/figma/url-parser.js.map +1 -0
- package/dist/src/platform/device-manager.d.ts +5 -0
- package/dist/src/platform/device-manager.d.ts.map +1 -1
- package/dist/src/platform/device-manager.js +18 -0
- package/dist/src/platform/device-manager.js.map +1 -1
- package/dist/src/platform/device-pool.d.ts +105 -0
- package/dist/src/platform/device-pool.d.ts.map +1 -0
- package/dist/src/platform/device-pool.js +328 -0
- package/dist/src/platform/device-pool.js.map +1 -0
- package/dist/src/platform/device-profiles.d.ts +50 -0
- package/dist/src/platform/device-profiles.d.ts.map +1 -0
- package/dist/src/platform/device-profiles.js +155 -0
- package/dist/src/platform/device-profiles.js.map +1 -0
- package/dist/src/platform/ios/ios-device.d.ts +0 -2
- package/dist/src/platform/ios/ios-device.d.ts.map +1 -1
- package/dist/src/platform/ios/ios-device.js +8 -18
- package/dist/src/platform/ios/ios-device.js.map +1 -1
- package/dist/src/platform/ios/simctl.d.ts +0 -5
- package/dist/src/platform/ios/simctl.d.ts.map +1 -1
- package/dist/src/platform/ios/simctl.js +0 -4
- package/dist/src/platform/ios/simctl.js.map +1 -1
- package/dist/src/platform/simulator-factory.d.ts +85 -0
- package/dist/src/platform/simulator-factory.d.ts.map +1 -0
- package/dist/src/platform/simulator-factory.js +396 -0
- package/dist/src/platform/simulator-factory.js.map +1 -0
- package/dist/src/platform/system-resources.d.ts +44 -0
- package/dist/src/platform/system-resources.d.ts.map +1 -0
- package/dist/src/platform/system-resources.js +103 -0
- package/dist/src/platform/system-resources.js.map +1 -0
- package/dist/src/prototype/browser-engine.d.ts +62 -0
- package/dist/src/prototype/browser-engine.d.ts.map +1 -0
- package/dist/src/prototype/browser-engine.js +199 -0
- package/dist/src/prototype/browser-engine.js.map +1 -0
- package/dist/src/prototype/figma-navigator.d.ts +40 -0
- package/dist/src/prototype/figma-navigator.d.ts.map +1 -0
- package/dist/src/prototype/figma-navigator.js +95 -0
- package/dist/src/prototype/figma-navigator.js.map +1 -0
- package/dist/src/prototype/flow-reporter.d.ts +47 -0
- package/dist/src/prototype/flow-reporter.d.ts.map +1 -0
- package/dist/src/prototype/flow-reporter.js +255 -0
- package/dist/src/prototype/flow-reporter.js.map +1 -0
- package/dist/src/prototype/screen-analyzer.d.ts +31 -0
- package/dist/src/prototype/screen-analyzer.d.ts.map +1 -0
- package/dist/src/prototype/screen-analyzer.js +114 -0
- package/dist/src/prototype/screen-analyzer.js.map +1 -0
- package/dist/src/prototype/web-crawler.d.ts +31 -0
- package/dist/src/prototype/web-crawler.d.ts.map +1 -0
- package/dist/src/prototype/web-crawler.js +88 -0
- package/dist/src/prototype/web-crawler.js.map +1 -0
- package/dist/src/reports/cross-device-report.d.ts +84 -0
- package/dist/src/reports/cross-device-report.d.ts.map +1 -0
- package/dist/src/reports/cross-device-report.js +342 -0
- package/dist/src/reports/cross-device-report.js.map +1 -0
- package/dist/src/server.d.ts.map +1 -1
- package/dist/src/server.js +9 -1
- package/dist/src/server.js.map +1 -1
- package/dist/src/snapshot/formatter.d.ts +5 -13
- package/dist/src/snapshot/formatter.d.ts.map +1 -1
- package/dist/src/snapshot/formatter.js +19 -28
- package/dist/src/snapshot/formatter.js.map +1 -1
- package/dist/src/tools/interaction-tools.d.ts +6 -6
- package/dist/src/tools/interaction-tools.d.ts.map +1 -1
- package/dist/src/tools/interaction-tools.js +2 -12
- package/dist/src/tools/interaction-tools.js.map +1 -1
- package/dist/src/tools/metro-tools.d.ts +2 -2
- package/dist/src/tools/multi-device-tools.d.ts +133 -0
- package/dist/src/tools/multi-device-tools.d.ts.map +1 -0
- package/dist/src/tools/multi-device-tools.js +365 -0
- package/dist/src/tools/multi-device-tools.js.map +1 -0
- package/dist/src/tools/navigation-tools.d.ts.map +1 -1
- package/dist/src/tools/navigation-tools.js +1 -6
- package/dist/src/tools/navigation-tools.js.map +1 -1
- package/dist/src/tools/prototype-tools.d.ts +102 -0
- package/dist/src/tools/prototype-tools.d.ts.map +1 -0
- package/dist/src/tools/prototype-tools.js +391 -0
- package/dist/src/tools/prototype-tools.js.map +1 -0
- package/dist/src/tools/screenshot-tools.d.ts +7 -74
- package/dist/src/tools/screenshot-tools.d.ts.map +1 -1
- package/dist/src/tools/screenshot-tools.js +19 -286
- package/dist/src/tools/screenshot-tools.js.map +1 -1
- package/dist/src/tools/vqa-tools.d.ts +2 -2
- package/dist/src/tools/vqa-tools.d.ts.map +1 -1
- package/dist/src/tools/vqa-tools.js +4 -69
- package/dist/src/tools/vqa-tools.js.map +1 -1
- package/docs/figma-workflow.md +58 -6
- package/docs/installation-guide.md +302 -0
- package/docs/setup-guide.md +77 -79
- package/docs/tool-reference.md +152 -69
- package/install.sh +119 -0
- package/package.json +5 -2
- package/setup.sh +320 -0
- package/tsconfig.json +1 -1
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BrowserEngine = void 0;
|
|
4
|
+
const DEFAULT_VIEWPORT = { width: 390, height: 844 };
|
|
5
|
+
const NAVIGATION_TIMEOUT_MS = 30_000;
|
|
6
|
+
const NETWORK_IDLE_TIMEOUT_MS = 5_000;
|
|
7
|
+
/**
|
|
8
|
+
* Thin wrapper around Puppeteer for headless Chrome lifecycle management.
|
|
9
|
+
* Lazily imports puppeteer so the rest of DevLens does not fail if the
|
|
10
|
+
* optional dependency is missing.
|
|
11
|
+
*/
|
|
12
|
+
class BrowserEngine {
|
|
13
|
+
browser = null;
|
|
14
|
+
page = null;
|
|
15
|
+
options;
|
|
16
|
+
constructor(options) {
|
|
17
|
+
this.options = {
|
|
18
|
+
viewportWidth: options?.viewportWidth ?? DEFAULT_VIEWPORT.width,
|
|
19
|
+
viewportHeight: options?.viewportHeight ?? DEFAULT_VIEWPORT.height,
|
|
20
|
+
headless: options?.headless ?? true,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
async launch() {
|
|
24
|
+
const puppeteer = await import("puppeteer");
|
|
25
|
+
this.browser = await puppeteer.default.launch({
|
|
26
|
+
headless: this.options.headless,
|
|
27
|
+
args: [
|
|
28
|
+
"--no-sandbox",
|
|
29
|
+
"--disable-setuid-sandbox",
|
|
30
|
+
"--disable-gpu",
|
|
31
|
+
"--disable-dev-shm-usage",
|
|
32
|
+
],
|
|
33
|
+
});
|
|
34
|
+
this.page = await this.browser.newPage();
|
|
35
|
+
await this.page.setViewport({
|
|
36
|
+
width: this.options.viewportWidth,
|
|
37
|
+
height: this.options.viewportHeight,
|
|
38
|
+
deviceScaleFactor: 2,
|
|
39
|
+
});
|
|
40
|
+
await this.page.setUserAgent("Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) " +
|
|
41
|
+
"AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 " +
|
|
42
|
+
"Mobile/15E148 Safari/604.1");
|
|
43
|
+
}
|
|
44
|
+
async navigate(url) {
|
|
45
|
+
const page = this.ensurePage();
|
|
46
|
+
await page.goto(url, {
|
|
47
|
+
waitUntil: "networkidle2",
|
|
48
|
+
timeout: NAVIGATION_TIMEOUT_MS,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
async screenshot() {
|
|
52
|
+
const page = this.ensurePage();
|
|
53
|
+
const data = await page.screenshot({
|
|
54
|
+
type: "png",
|
|
55
|
+
fullPage: true,
|
|
56
|
+
encoding: "binary",
|
|
57
|
+
});
|
|
58
|
+
return Buffer.from(data);
|
|
59
|
+
}
|
|
60
|
+
async viewportScreenshot() {
|
|
61
|
+
const page = this.ensurePage();
|
|
62
|
+
const data = await page.screenshot({
|
|
63
|
+
type: "png",
|
|
64
|
+
fullPage: false,
|
|
65
|
+
encoding: "binary",
|
|
66
|
+
});
|
|
67
|
+
return Buffer.from(data);
|
|
68
|
+
}
|
|
69
|
+
async scrollPage(direction, distance = 500) {
|
|
70
|
+
const page = this.ensurePage();
|
|
71
|
+
const d = direction === "down" ? distance : -distance;
|
|
72
|
+
await page.evaluate((scrollY) => {
|
|
73
|
+
window.scrollBy({ top: scrollY, behavior: "smooth" });
|
|
74
|
+
}, d);
|
|
75
|
+
await this.waitMs(400);
|
|
76
|
+
}
|
|
77
|
+
async getScrollHeight() {
|
|
78
|
+
const page = this.ensurePage();
|
|
79
|
+
return page.evaluate(() => document.body.scrollHeight);
|
|
80
|
+
}
|
|
81
|
+
async getScrollPosition() {
|
|
82
|
+
const page = this.ensurePage();
|
|
83
|
+
return page.evaluate(() => window.scrollY);
|
|
84
|
+
}
|
|
85
|
+
async click(x, y) {
|
|
86
|
+
const page = this.ensurePage();
|
|
87
|
+
await page.mouse.click(x, y);
|
|
88
|
+
}
|
|
89
|
+
async clickElement(selector) {
|
|
90
|
+
const page = this.ensurePage();
|
|
91
|
+
try {
|
|
92
|
+
await page.click(selector);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async extractLinks() {
|
|
100
|
+
const page = this.ensurePage();
|
|
101
|
+
return page.evaluate(() => {
|
|
102
|
+
const origin = window.location.origin;
|
|
103
|
+
const anchors = Array.from(document.querySelectorAll("a[href]"));
|
|
104
|
+
const seen = new Set();
|
|
105
|
+
const results = [];
|
|
106
|
+
for (const a of anchors) {
|
|
107
|
+
const href = a.href;
|
|
108
|
+
if (!href.startsWith(origin))
|
|
109
|
+
continue;
|
|
110
|
+
const normalized = href.split("#")[0].split("?")[0];
|
|
111
|
+
if (seen.has(normalized))
|
|
112
|
+
continue;
|
|
113
|
+
seen.add(normalized);
|
|
114
|
+
results.push({
|
|
115
|
+
text: (a.textContent || "").trim().slice(0, 80),
|
|
116
|
+
href,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return results;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
async extractDomMetadata() {
|
|
123
|
+
const page = this.ensurePage();
|
|
124
|
+
return page.evaluate(() => {
|
|
125
|
+
const title = document.title || "";
|
|
126
|
+
const headings = Array.from(document.querySelectorAll("h1, h2, h3, h4")).map((el) => (el.textContent || "").trim().slice(0, 120));
|
|
127
|
+
const origin = window.location.origin;
|
|
128
|
+
const links = Array.from(document.querySelectorAll("a[href]"))
|
|
129
|
+
.filter((a) => a.href.startsWith(origin))
|
|
130
|
+
.map((a) => ({
|
|
131
|
+
text: (a.textContent || "").trim().slice(0, 80),
|
|
132
|
+
href: a.href,
|
|
133
|
+
}));
|
|
134
|
+
const images = Array.from(document.querySelectorAll("img"))
|
|
135
|
+
.slice(0, 30)
|
|
136
|
+
.map((img) => ({
|
|
137
|
+
src: img.src?.slice(0, 200) || "",
|
|
138
|
+
alt: img.alt || "",
|
|
139
|
+
}));
|
|
140
|
+
const interactiveSelectors = "button, [role=button], input, select, textarea, [onclick], a";
|
|
141
|
+
const interactiveElements = Array.from(document.querySelectorAll(interactiveSelectors))
|
|
142
|
+
.slice(0, 50)
|
|
143
|
+
.map((el) => {
|
|
144
|
+
const rect = el.getBoundingClientRect();
|
|
145
|
+
return {
|
|
146
|
+
tag: el.tagName.toLowerCase(),
|
|
147
|
+
text: (el.textContent || "").trim().slice(0, 80),
|
|
148
|
+
href: el.href || undefined,
|
|
149
|
+
role: el.getAttribute("role") || undefined,
|
|
150
|
+
bounds: {
|
|
151
|
+
x: Math.round(rect.x),
|
|
152
|
+
y: Math.round(rect.y),
|
|
153
|
+
width: Math.round(rect.width),
|
|
154
|
+
height: Math.round(rect.height),
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
});
|
|
158
|
+
const textContent = (document.body.innerText || "")
|
|
159
|
+
.replace(/\s+/g, " ")
|
|
160
|
+
.trim()
|
|
161
|
+
.slice(0, 2000);
|
|
162
|
+
return { title, headings, links, images, interactiveElements, textContent };
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
async waitForNavigation() {
|
|
166
|
+
const page = this.ensurePage();
|
|
167
|
+
try {
|
|
168
|
+
await page.waitForNavigation({
|
|
169
|
+
waitUntil: "networkidle2",
|
|
170
|
+
timeout: NETWORK_IDLE_TIMEOUT_MS,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
/* timeout is acceptable – the page may not navigate */
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async waitMs(ms) {
|
|
178
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
179
|
+
}
|
|
180
|
+
async currentUrl() {
|
|
181
|
+
const page = this.ensurePage();
|
|
182
|
+
return page.url();
|
|
183
|
+
}
|
|
184
|
+
async close() {
|
|
185
|
+
if (this.browser) {
|
|
186
|
+
await this.browser.close();
|
|
187
|
+
this.browser = null;
|
|
188
|
+
this.page = null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
ensurePage() {
|
|
192
|
+
if (!this.page) {
|
|
193
|
+
throw new Error("Browser not launched. Call launch() before using the engine.");
|
|
194
|
+
}
|
|
195
|
+
return this.page;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
exports.BrowserEngine = BrowserEngine;
|
|
199
|
+
//# sourceMappingURL=browser-engine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-engine.js","sourceRoot":"","sources":["../../../src/prototype/browser-engine.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { BrowserEngine } from "./browser-engine.js";
|
|
2
|
+
import type { PrototypeFlowSummary } from "../figma/figma-api.js";
|
|
3
|
+
export interface NavigationStep {
|
|
4
|
+
screenId: string;
|
|
5
|
+
screenName: string;
|
|
6
|
+
screenshot: Buffer;
|
|
7
|
+
fromScreenId?: string;
|
|
8
|
+
trigger?: string;
|
|
9
|
+
transitionType?: string;
|
|
10
|
+
transitionDuration?: number;
|
|
11
|
+
}
|
|
12
|
+
export interface FigmaNavigatorOptions {
|
|
13
|
+
maxScreens: number;
|
|
14
|
+
captureDelay: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Navigate a Figma prototype by opening the prototype URL in Puppeteer
|
|
18
|
+
* and using the flow graph from the Figma REST API to determine where
|
|
19
|
+
* to click.
|
|
20
|
+
*
|
|
21
|
+
* Because the Figma prototype viewer renders inside a `<canvas>`,
|
|
22
|
+
* DOM-based hotspot detection is not possible. We rely on the prototype
|
|
23
|
+
* starting state and advance by clicking in the centre of the viewport
|
|
24
|
+
* (the most common hotspot zone) or at the approximate coordinate
|
|
25
|
+
* derived from the connection metadata.
|
|
26
|
+
*/
|
|
27
|
+
export declare class FigmaNavigator {
|
|
28
|
+
private readonly engine;
|
|
29
|
+
private readonly options;
|
|
30
|
+
constructor(engine: BrowserEngine, options: FigmaNavigatorOptions);
|
|
31
|
+
navigatePrototype(protoUrl: string, flow: PrototypeFlowSummary): Promise<NavigationStep[]>;
|
|
32
|
+
/**
|
|
33
|
+
* Advance the Figma prototype by clicking the centre of the viewport.
|
|
34
|
+
* Figma prototype hotspots are typically full-frame overlays or large
|
|
35
|
+
* CTA areas. A centre-click covers the majority of single-hotspot
|
|
36
|
+
* frames.
|
|
37
|
+
*/
|
|
38
|
+
private advancePrototype;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=figma-navigator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"figma-navigator.d.ts","sourceRoot":"","sources":["../../../src/prototype/figma-navigator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,KAAK,EACV,oBAAoB,EAErB,MAAM,uBAAuB,CAAC;AAE/B,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;CACtB;AAkBD;;;;;;;;;;GAUG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgB;IACvC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;gBAEpC,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,qBAAqB;IAK3D,iBAAiB,CACrB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,oBAAoB,GACzB,OAAO,CAAC,cAAc,EAAE,CAAC;IA0D5B;;;;;OAKG;YACW,gBAAgB;CAM/B"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FigmaNavigator = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Build an adjacency list from prototype connections so we can
|
|
6
|
+
* BFS through the flow graph.
|
|
7
|
+
*/
|
|
8
|
+
function buildAdjacency(connections) {
|
|
9
|
+
const adj = new Map();
|
|
10
|
+
for (const conn of connections) {
|
|
11
|
+
const list = adj.get(conn.sourceNodeId) || [];
|
|
12
|
+
list.push(conn);
|
|
13
|
+
adj.set(conn.sourceNodeId, list);
|
|
14
|
+
}
|
|
15
|
+
return adj;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Navigate a Figma prototype by opening the prototype URL in Puppeteer
|
|
19
|
+
* and using the flow graph from the Figma REST API to determine where
|
|
20
|
+
* to click.
|
|
21
|
+
*
|
|
22
|
+
* Because the Figma prototype viewer renders inside a `<canvas>`,
|
|
23
|
+
* DOM-based hotspot detection is not possible. We rely on the prototype
|
|
24
|
+
* starting state and advance by clicking in the centre of the viewport
|
|
25
|
+
* (the most common hotspot zone) or at the approximate coordinate
|
|
26
|
+
* derived from the connection metadata.
|
|
27
|
+
*/
|
|
28
|
+
class FigmaNavigator {
|
|
29
|
+
engine;
|
|
30
|
+
options;
|
|
31
|
+
constructor(engine, options) {
|
|
32
|
+
this.engine = engine;
|
|
33
|
+
this.options = options;
|
|
34
|
+
}
|
|
35
|
+
async navigatePrototype(protoUrl, flow) {
|
|
36
|
+
const steps = [];
|
|
37
|
+
const adjacency = buildAdjacency(flow.connections);
|
|
38
|
+
const visited = new Set();
|
|
39
|
+
await this.engine.navigate(protoUrl);
|
|
40
|
+
await this.engine.waitMs(this.options.captureDelay + 1000);
|
|
41
|
+
const startId = flow.startingNodeId || flow.allScreenIds[0];
|
|
42
|
+
if (!startId)
|
|
43
|
+
return steps;
|
|
44
|
+
const startName = flow.startingNodeName ||
|
|
45
|
+
flow.allScreenNames.get(startId) ||
|
|
46
|
+
"Start Screen";
|
|
47
|
+
steps.push({
|
|
48
|
+
screenId: startId,
|
|
49
|
+
screenName: startName,
|
|
50
|
+
screenshot: await this.engine.viewportScreenshot(),
|
|
51
|
+
});
|
|
52
|
+
visited.add(startId);
|
|
53
|
+
const queue = [startId];
|
|
54
|
+
while (queue.length > 0 && steps.length < this.options.maxScreens) {
|
|
55
|
+
const currentId = queue.shift();
|
|
56
|
+
const edges = adjacency.get(currentId) || [];
|
|
57
|
+
for (const edge of edges) {
|
|
58
|
+
if (visited.has(edge.destinationNodeId))
|
|
59
|
+
continue;
|
|
60
|
+
if (steps.length >= this.options.maxScreens)
|
|
61
|
+
break;
|
|
62
|
+
await this.advancePrototype();
|
|
63
|
+
await this.engine.waitMs(this.options.captureDelay);
|
|
64
|
+
const destName = flow.allScreenNames.get(edge.destinationNodeId) ||
|
|
65
|
+
`Screen ${steps.length + 1}`;
|
|
66
|
+
steps.push({
|
|
67
|
+
screenId: edge.destinationNodeId,
|
|
68
|
+
screenName: destName,
|
|
69
|
+
screenshot: await this.engine.viewportScreenshot(),
|
|
70
|
+
fromScreenId: currentId,
|
|
71
|
+
trigger: edge.trigger,
|
|
72
|
+
transitionType: edge.transitionType,
|
|
73
|
+
transitionDuration: edge.transitionDuration,
|
|
74
|
+
});
|
|
75
|
+
visited.add(edge.destinationNodeId);
|
|
76
|
+
queue.push(edge.destinationNodeId);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return steps;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Advance the Figma prototype by clicking the centre of the viewport.
|
|
83
|
+
* Figma prototype hotspots are typically full-frame overlays or large
|
|
84
|
+
* CTA areas. A centre-click covers the majority of single-hotspot
|
|
85
|
+
* frames.
|
|
86
|
+
*/
|
|
87
|
+
async advancePrototype() {
|
|
88
|
+
const vw = 390;
|
|
89
|
+
const vh = 844;
|
|
90
|
+
await this.engine.click(Math.round(vw / 2), Math.round(vh / 2));
|
|
91
|
+
await this.engine.waitMs(800);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
exports.FigmaNavigator = FigmaNavigator;
|
|
95
|
+
//# sourceMappingURL=figma-navigator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"figma-navigator.js","sourceRoot":"","sources":["../../../src/prototype/figma-navigator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { CrawlResult } from "./web-crawler.js";
|
|
2
|
+
import type { NavigationStep } from "./figma-navigator.js";
|
|
3
|
+
import type { ScreenAnalysis } from "./screen-analyzer.js";
|
|
4
|
+
export interface ScreenReport {
|
|
5
|
+
name: string;
|
|
6
|
+
url?: string;
|
|
7
|
+
screenshot: Buffer;
|
|
8
|
+
analysis: ScreenAnalysis;
|
|
9
|
+
transitionFrom?: string;
|
|
10
|
+
trigger?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface FlowReport {
|
|
13
|
+
type: "web" | "figma";
|
|
14
|
+
totalScreens: number;
|
|
15
|
+
screens: ScreenReport[];
|
|
16
|
+
flowDiagram: string;
|
|
17
|
+
designTokens: DesignTokenSummary;
|
|
18
|
+
componentInventory: string[];
|
|
19
|
+
actionNeeded?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface DesignTokenSummary {
|
|
22
|
+
colors: Array<{
|
|
23
|
+
hex: string;
|
|
24
|
+
percentage: number;
|
|
25
|
+
}>;
|
|
26
|
+
headings: string[];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Build a flow report from web crawler results and per-screen analyses.
|
|
30
|
+
*/
|
|
31
|
+
export declare function buildWebFlowReport(crawlResults: CrawlResult[], analyses: ScreenAnalysis[], missingFigmaUrl: boolean): FlowReport;
|
|
32
|
+
/**
|
|
33
|
+
* Build a flow report from Figma navigator steps and per-screen analyses.
|
|
34
|
+
*/
|
|
35
|
+
export declare function buildFigmaFlowReport(steps: NavigationStep[], analyses: ScreenAnalysis[], missingDesignUrl: boolean): FlowReport;
|
|
36
|
+
/**
|
|
37
|
+
* Format a FlowReport into MCP-compatible content blocks.
|
|
38
|
+
*/
|
|
39
|
+
export declare function formatFlowReportContent(report: FlowReport): Array<{
|
|
40
|
+
type: "text";
|
|
41
|
+
text: string;
|
|
42
|
+
} | {
|
|
43
|
+
type: "image";
|
|
44
|
+
data: string;
|
|
45
|
+
mimeType: string;
|
|
46
|
+
}>;
|
|
47
|
+
//# sourceMappingURL=flow-reporter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flow-reporter.d.ts","sourceRoot":"","sources":["../../../src/prototype/flow-reporter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAG3D,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,cAAc,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,KAAK,GAAG,OAAO,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,kBAAkB,CAAC;IACjC,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAC7B,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACnD,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAChC,YAAY,EAAE,WAAW,EAAE,EAC3B,QAAQ,EAAE,cAAc,EAAE,EAC1B,eAAe,EAAE,OAAO,GACvB,UAAU,CAgCZ;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,cAAc,EAAE,EACvB,QAAQ,EAAE,cAAc,EAAE,EAC1B,gBAAgB,EAAE,OAAO,GACxB,UAAU,CA+BZ;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,UAAU,GACjB,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAiG3F"}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildWebFlowReport = buildWebFlowReport;
|
|
4
|
+
exports.buildFigmaFlowReport = buildFigmaFlowReport;
|
|
5
|
+
exports.formatFlowReportContent = formatFlowReportContent;
|
|
6
|
+
/**
|
|
7
|
+
* Build a flow report from web crawler results and per-screen analyses.
|
|
8
|
+
*/
|
|
9
|
+
function buildWebFlowReport(crawlResults, analyses, missingFigmaUrl) {
|
|
10
|
+
const screens = crawlResults.map((cr, idx) => ({
|
|
11
|
+
name: cr.path === "/" ? "Home" : cr.path,
|
|
12
|
+
url: cr.url,
|
|
13
|
+
screenshot: cr.screenshot,
|
|
14
|
+
analysis: analyses[idx] || emptyAnalysis(),
|
|
15
|
+
}));
|
|
16
|
+
const flowDiagram = buildWebFlowDiagram(crawlResults);
|
|
17
|
+
const designTokens = aggregateDesignTokens(analyses);
|
|
18
|
+
const componentInventory = aggregateComponents(analyses);
|
|
19
|
+
let actionNeeded;
|
|
20
|
+
if (missingFigmaUrl) {
|
|
21
|
+
actionNeeded =
|
|
22
|
+
"ACTION NEEDED: Provide the Figma design link so I can extract exact " +
|
|
23
|
+
"design specs. Ask the developer:\n" +
|
|
24
|
+
"'Is there a Figma file for this prototype? I need it for pixel-perfect specs " +
|
|
25
|
+
"(colors, spacing, typography, component structure).'\n\n" +
|
|
26
|
+
"Without Figma: visual patterns extracted from screenshots (~85% accuracy).\n" +
|
|
27
|
+
"With Figma: exact design tokens + flow = near-100% accuracy.";
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
type: "web",
|
|
31
|
+
totalScreens: screens.length,
|
|
32
|
+
screens,
|
|
33
|
+
flowDiagram,
|
|
34
|
+
designTokens,
|
|
35
|
+
componentInventory,
|
|
36
|
+
actionNeeded,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Build a flow report from Figma navigator steps and per-screen analyses.
|
|
41
|
+
*/
|
|
42
|
+
function buildFigmaFlowReport(steps, analyses, missingDesignUrl) {
|
|
43
|
+
const screens = steps.map((step, idx) => ({
|
|
44
|
+
name: step.screenName,
|
|
45
|
+
screenshot: step.screenshot,
|
|
46
|
+
analysis: analyses[idx] || emptyAnalysis(),
|
|
47
|
+
transitionFrom: step.fromScreenId
|
|
48
|
+
? steps.find((s) => s.screenId === step.fromScreenId)?.screenName
|
|
49
|
+
: undefined,
|
|
50
|
+
trigger: step.trigger,
|
|
51
|
+
}));
|
|
52
|
+
const flowDiagram = buildFigmaFlowDiagram(steps);
|
|
53
|
+
const designTokens = aggregateDesignTokens(analyses);
|
|
54
|
+
const componentInventory = aggregateComponents(analyses);
|
|
55
|
+
let actionNeeded;
|
|
56
|
+
if (missingDesignUrl) {
|
|
57
|
+
actionNeeded =
|
|
58
|
+
"ACTION NEEDED: Also provide the Figma DESIGN link (figma.com/design/...) " +
|
|
59
|
+
"so I can extract exact design specs alongside the prototype flow.";
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
type: "figma",
|
|
63
|
+
totalScreens: screens.length,
|
|
64
|
+
screens,
|
|
65
|
+
flowDiagram,
|
|
66
|
+
designTokens,
|
|
67
|
+
componentInventory,
|
|
68
|
+
actionNeeded,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Format a FlowReport into MCP-compatible content blocks.
|
|
73
|
+
*/
|
|
74
|
+
function formatFlowReportContent(report) {
|
|
75
|
+
const content = [];
|
|
76
|
+
const headerLines = [
|
|
77
|
+
`# Prototype Exploration Report`,
|
|
78
|
+
``,
|
|
79
|
+
`Type: ${report.type === "web" ? "Web-hosted prototype" : "Figma prototype"}`,
|
|
80
|
+
`Total screens: ${report.totalScreens}`,
|
|
81
|
+
``,
|
|
82
|
+
`## Flow Diagram`,
|
|
83
|
+
report.flowDiagram,
|
|
84
|
+
``,
|
|
85
|
+
];
|
|
86
|
+
if (report.designTokens.colors.length > 0) {
|
|
87
|
+
headerLines.push(`## Design Tokens (extracted)`);
|
|
88
|
+
headerLines.push(`### Colors`);
|
|
89
|
+
for (const c of report.designTokens.colors.slice(0, 10)) {
|
|
90
|
+
headerLines.push(` ${c.hex} (${c.percentage.toFixed(1)}%)`);
|
|
91
|
+
}
|
|
92
|
+
headerLines.push(``);
|
|
93
|
+
}
|
|
94
|
+
if (report.designTokens.headings.length > 0) {
|
|
95
|
+
headerLines.push(`### Typography (headings found)`);
|
|
96
|
+
for (const h of report.designTokens.headings.slice(0, 15)) {
|
|
97
|
+
headerLines.push(` - ${h}`);
|
|
98
|
+
}
|
|
99
|
+
headerLines.push(``);
|
|
100
|
+
}
|
|
101
|
+
if (report.componentInventory.length > 0) {
|
|
102
|
+
headerLines.push(`## Component Inventory`);
|
|
103
|
+
for (const c of report.componentInventory) {
|
|
104
|
+
headerLines.push(` - ${c}`);
|
|
105
|
+
}
|
|
106
|
+
headerLines.push(``);
|
|
107
|
+
}
|
|
108
|
+
if (report.actionNeeded) {
|
|
109
|
+
headerLines.push(`---`);
|
|
110
|
+
headerLines.push(report.actionNeeded);
|
|
111
|
+
headerLines.push(``);
|
|
112
|
+
}
|
|
113
|
+
content.push({ type: "text", text: headerLines.join("\n") });
|
|
114
|
+
for (const screen of report.screens) {
|
|
115
|
+
const screenLines = [
|
|
116
|
+
`---`,
|
|
117
|
+
`### Screen: ${screen.name}`,
|
|
118
|
+
];
|
|
119
|
+
if (screen.url) {
|
|
120
|
+
screenLines.push(`URL: ${screen.url}`);
|
|
121
|
+
}
|
|
122
|
+
if (screen.transitionFrom) {
|
|
123
|
+
screenLines.push(`Transition: from "${screen.transitionFrom}" via ${screen.trigger || "tap"}`);
|
|
124
|
+
}
|
|
125
|
+
const a = screen.analysis;
|
|
126
|
+
if (a.colorPalette.length > 0) {
|
|
127
|
+
screenLines.push(`Colors: ${a.colorPalette.slice(0, 5).map((c) => c.hex).join(", ")}`);
|
|
128
|
+
}
|
|
129
|
+
if (a.sectionCount > 0) {
|
|
130
|
+
screenLines.push(`Sections: ${a.sectionCount}`);
|
|
131
|
+
}
|
|
132
|
+
if (a.interactiveElements.length > 0) {
|
|
133
|
+
screenLines.push(`Interactive elements: ${a.interactiveElements.length}`);
|
|
134
|
+
}
|
|
135
|
+
if (a.components.length > 0) {
|
|
136
|
+
screenLines.push(`Components: ${a.components.map((c) => `${c.type}(${c.count})`).join(", ")}`);
|
|
137
|
+
}
|
|
138
|
+
if (a.navigationElements.length > 0) {
|
|
139
|
+
screenLines.push(`Nav: ${a.navigationElements.map((n) => n.text).join(", ")}`);
|
|
140
|
+
}
|
|
141
|
+
content.push({ type: "text", text: screenLines.join("\n") });
|
|
142
|
+
content.push({
|
|
143
|
+
type: "image",
|
|
144
|
+
data: screen.screenshot.toString("base64"),
|
|
145
|
+
mimeType: "image/png",
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return content;
|
|
149
|
+
}
|
|
150
|
+
function buildWebFlowDiagram(results) {
|
|
151
|
+
if (results.length === 0)
|
|
152
|
+
return "(no screens found)";
|
|
153
|
+
const lines = [];
|
|
154
|
+
const homePage = results[0];
|
|
155
|
+
for (const result of results) {
|
|
156
|
+
const label = result.path === "/" ? "Home" : result.path;
|
|
157
|
+
const linkCount = result.domMetadata.links.length;
|
|
158
|
+
const interactiveCount = result.domMetadata.interactiveElements.length;
|
|
159
|
+
lines.push(` [${label}] (${linkCount} links, ${interactiveCount} interactive)`);
|
|
160
|
+
}
|
|
161
|
+
const connections = [];
|
|
162
|
+
for (const result of results) {
|
|
163
|
+
const from = result.path === "/" ? "Home" : result.path;
|
|
164
|
+
for (const link of result.domMetadata.links) {
|
|
165
|
+
const toPath = extractPath(link.href);
|
|
166
|
+
const matchingScreen = results.find((r) => r.path === toPath);
|
|
167
|
+
if (matchingScreen && matchingScreen.path !== result.path) {
|
|
168
|
+
const to = matchingScreen.path === "/" ? "Home" : matchingScreen.path;
|
|
169
|
+
connections.push(` ${from} --(${link.text || "link"})--> ${to}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return [
|
|
174
|
+
"Screens:",
|
|
175
|
+
...lines,
|
|
176
|
+
"",
|
|
177
|
+
"Flow connections:",
|
|
178
|
+
...deduplicateStrings(connections).slice(0, 30),
|
|
179
|
+
].join("\n");
|
|
180
|
+
}
|
|
181
|
+
function buildFigmaFlowDiagram(steps) {
|
|
182
|
+
if (steps.length === 0)
|
|
183
|
+
return "(no screens found)";
|
|
184
|
+
const lines = ["Screens:"];
|
|
185
|
+
for (const step of steps) {
|
|
186
|
+
lines.push(` [${step.screenName}] (id: ${step.screenId})`);
|
|
187
|
+
}
|
|
188
|
+
lines.push("", "Flow connections:");
|
|
189
|
+
for (const step of steps) {
|
|
190
|
+
if (step.fromScreenId) {
|
|
191
|
+
const fromName = steps.find((s) => s.screenId === step.fromScreenId)?.screenName ||
|
|
192
|
+
step.fromScreenId;
|
|
193
|
+
const trigger = step.trigger || "tap";
|
|
194
|
+
const transition = step.transitionType
|
|
195
|
+
? ` (${step.transitionType}${step.transitionDuration ? ` ${step.transitionDuration}ms` : ""})`
|
|
196
|
+
: "";
|
|
197
|
+
lines.push(` ${fromName} --(${trigger}${transition})--> ${step.screenName}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return lines.join("\n");
|
|
201
|
+
}
|
|
202
|
+
function aggregateDesignTokens(analyses) {
|
|
203
|
+
const colorMap = new Map();
|
|
204
|
+
const allHeadings = [];
|
|
205
|
+
for (const a of analyses) {
|
|
206
|
+
for (const c of a.colorPalette) {
|
|
207
|
+
colorMap.set(c.hex, (colorMap.get(c.hex) || 0) + c.percentage);
|
|
208
|
+
}
|
|
209
|
+
for (const t of a.typography) {
|
|
210
|
+
allHeadings.push(t.text);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const colors = [...colorMap.entries()]
|
|
214
|
+
.sort((a, b) => b[1] - a[1])
|
|
215
|
+
.slice(0, 10)
|
|
216
|
+
.map(([hex, percentage]) => ({ hex, percentage }));
|
|
217
|
+
return { colors, headings: deduplicateStrings(allHeadings) };
|
|
218
|
+
}
|
|
219
|
+
function aggregateComponents(analyses) {
|
|
220
|
+
const seen = new Set();
|
|
221
|
+
const items = [];
|
|
222
|
+
for (const a of analyses) {
|
|
223
|
+
for (const c of a.components) {
|
|
224
|
+
const key = `${c.type}`;
|
|
225
|
+
if (!seen.has(key)) {
|
|
226
|
+
seen.add(key);
|
|
227
|
+
items.push(`${c.type} (found ${c.count}x, e.g. ${c.examples.slice(0, 2).join(", ")})`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return items;
|
|
232
|
+
}
|
|
233
|
+
function extractPath(url) {
|
|
234
|
+
try {
|
|
235
|
+
return new URL(url).pathname.replace(/\/+$/, "") || "/";
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return "/";
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function deduplicateStrings(arr) {
|
|
242
|
+
return [...new Set(arr)];
|
|
243
|
+
}
|
|
244
|
+
function emptyAnalysis() {
|
|
245
|
+
return {
|
|
246
|
+
colorPalette: [],
|
|
247
|
+
typography: [],
|
|
248
|
+
components: [],
|
|
249
|
+
interactiveElements: [],
|
|
250
|
+
navigationElements: [],
|
|
251
|
+
scrollDepth: 0,
|
|
252
|
+
sectionCount: 0,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
//# sourceMappingURL=flow-reporter.js.map
|