visual-mirror 1.0.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 +109 -0
- package/dist/analyze.d.ts +22 -0
- package/dist/analyze.js +105 -0
- package/dist/analyze.js.map +1 -0
- package/dist/capture.d.ts +1 -0
- package/dist/capture.js +41 -0
- package/dist/capture.js.map +1 -0
- package/dist/clipboard.d.ts +1 -0
- package/dist/clipboard.js +109 -0
- package/dist/clipboard.js.map +1 -0
- package/dist/diff.d.ts +5 -0
- package/dist/diff.js +62 -0
- package/dist/diff.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +181 -0
- package/dist/index.js.map +1 -0
- package/dist/interactive.d.ts +2 -0
- package/dist/interactive.js +84 -0
- package/dist/interactive.js.map +1 -0
- package/dist/report.d.ts +2 -0
- package/dist/report.js +126 -0
- package/dist/report.js.map +1 -0
- package/package.json +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# visual-mirror
|
|
2
|
+
|
|
3
|
+
> Pixel diffs tell you something changed. Visual Mirror tells you what it means.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/visual-mirror)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
A CLI tool for visual regression testing powered by AI. It captures a screenshot of a live URL, diffs it pixel-by-pixel against a reference image, then uses Claude Vision to provide an intelligent, human-readable analysis of what changed — complete with actionable fix suggestions.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install -g visual-mirror
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# Option 1: Paste from clipboard — just copy a screenshot and run:
|
|
20
|
+
visual-mirror --url http://localhost:3000
|
|
21
|
+
|
|
22
|
+
# Option 2: Provide a reference file:
|
|
23
|
+
visual-mirror --ref ./reference.png --url http://localhost:3000
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## How It Works
|
|
27
|
+
|
|
28
|
+
1. **Capture** — Takes a headless Chromium screenshot of your live URL via Playwright
|
|
29
|
+
2. **Diff** — Compares the captured screenshot against your reference image pixel-by-pixel using Jimp, generating a visual diff overlay
|
|
30
|
+
3. **Analyze** — Sends both screenshots to Claude Vision, which identifies specific UI issues (not just "something changed" — it tells you *what* changed)
|
|
31
|
+
4. **Select** — Presents issues in an interactive terminal prompt (like `yay` on Arch) where you pick which ones to get fix suggestions for
|
|
32
|
+
5. **Report** — Generates a self-contained HTML report with all three images, issues, and fix suggestions, then auto-opens it in your browser
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
visual-mirror --ref <path> --url <url> [options]
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Options
|
|
41
|
+
|
|
42
|
+
| Flag | Description | Default |
|
|
43
|
+
|------|-------------|---------|
|
|
44
|
+
| `--ref <path>` | Path to reference screenshot (PNG/JPG). If omitted, reads from clipboard | *optional* |
|
|
45
|
+
| `--url <url>` | URL to capture and compare | *required* |
|
|
46
|
+
| `--width <number>` | Viewport width in pixels | `1280` |
|
|
47
|
+
| `--height <number>` | Viewport height in pixels | `720` |
|
|
48
|
+
| `--out <path>` | Output directory for report | `./visual-mirror-report` |
|
|
49
|
+
|
|
50
|
+
### Example
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
visual-mirror --ref ./designs/homepage.png --url http://localhost:3000 --width 1440 --height 900
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Interactive Flow
|
|
57
|
+
|
|
58
|
+
After analysis, you'll see something like:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
Severity: MINOR
|
|
62
|
+
|
|
63
|
+
Issues found:
|
|
64
|
+
1 Button alignment shifted 8px to the right .cta-button
|
|
65
|
+
2 Font color changed from #333 to #555 body
|
|
66
|
+
3 Hero image 12px taller than reference .hero-image
|
|
67
|
+
|
|
68
|
+
==> Select issues to get fix suggestions for (e.g. 1,2 or * for all)
|
|
69
|
+
==> 1,3
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Then you'll get targeted fix suggestions:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
Fix suggestions:
|
|
76
|
+
|
|
77
|
+
[1] Button alignment shifted 8px to the right
|
|
78
|
+
→ Check margin-right on .cta-button, likely a padding change.
|
|
79
|
+
Try: padding-right: 16px;
|
|
80
|
+
|
|
81
|
+
[3] Hero image 12px taller than reference
|
|
82
|
+
→ Set an explicit height on .hero-image: height: 320px;
|
|
83
|
+
|
|
84
|
+
✔ Report saved to ./visual-mirror-report/index.html
|
|
85
|
+
Opening report...
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Environment Setup
|
|
89
|
+
|
|
90
|
+
Set your Anthropic API key:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
export ANTHROPIC_API_KEY="your-api-key-here"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Get one at [console.anthropic.com](https://console.anthropic.com).
|
|
97
|
+
|
|
98
|
+
## Publishing
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
npm login
|
|
102
|
+
npm publish
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The `prepublishOnly` script runs `tsc` automatically before publishing.
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
MIT
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type Issue = {
|
|
2
|
+
id: number;
|
|
3
|
+
title: string;
|
|
4
|
+
description: string;
|
|
5
|
+
area: string;
|
|
6
|
+
};
|
|
7
|
+
export type Severity = "OK" | "MINOR" | "MAJOR";
|
|
8
|
+
export type AnalysisResult = {
|
|
9
|
+
severity: Severity;
|
|
10
|
+
verdict: string;
|
|
11
|
+
issues: Issue[];
|
|
12
|
+
};
|
|
13
|
+
export type Fix = {
|
|
14
|
+
id: number;
|
|
15
|
+
title: string;
|
|
16
|
+
suggestion: string;
|
|
17
|
+
};
|
|
18
|
+
export type FixResult = {
|
|
19
|
+
fixes: Fix[];
|
|
20
|
+
};
|
|
21
|
+
export declare function analyzeScreenshots(referencePath: string, capturedPath: string): Promise<AnalysisResult>;
|
|
22
|
+
export declare function getFixSuggestions(issues: Issue[]): Promise<FixResult>;
|
package/dist/analyze.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
function imageToBase64(filePath) {
|
|
5
|
+
return fs.readFileSync(filePath).toString("base64");
|
|
6
|
+
}
|
|
7
|
+
function getMediaType(filePath) {
|
|
8
|
+
const ext = filePath.toLowerCase().split(".").pop();
|
|
9
|
+
if (ext === "jpg" || ext === "jpeg")
|
|
10
|
+
return "image/jpeg";
|
|
11
|
+
if (ext === "webp")
|
|
12
|
+
return "image/webp";
|
|
13
|
+
if (ext === "gif")
|
|
14
|
+
return "image/gif";
|
|
15
|
+
return "image/png";
|
|
16
|
+
}
|
|
17
|
+
export async function analyzeScreenshots(referencePath, capturedPath) {
|
|
18
|
+
const client = new Anthropic();
|
|
19
|
+
const refBase64 = imageToBase64(referencePath);
|
|
20
|
+
const capBase64 = imageToBase64(capturedPath);
|
|
21
|
+
const refMediaType = getMediaType(referencePath);
|
|
22
|
+
const capMediaType = getMediaType(capturedPath);
|
|
23
|
+
const response = await client.messages.create({
|
|
24
|
+
model: "claude-sonnet-4-20250514",
|
|
25
|
+
max_tokens: 4096,
|
|
26
|
+
system: "You are a visual QA engineer reviewing UI screenshots.",
|
|
27
|
+
messages: [
|
|
28
|
+
{
|
|
29
|
+
role: "user",
|
|
30
|
+
content: [
|
|
31
|
+
{
|
|
32
|
+
type: "image",
|
|
33
|
+
source: {
|
|
34
|
+
type: "base64",
|
|
35
|
+
media_type: refMediaType,
|
|
36
|
+
data: refBase64,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
type: "image",
|
|
41
|
+
source: {
|
|
42
|
+
type: "base64",
|
|
43
|
+
media_type: capMediaType,
|
|
44
|
+
data: capBase64,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
type: "text",
|
|
49
|
+
text: `Compare these two screenshots. First is the reference/expected design. Second is the actual rendered result.
|
|
50
|
+
Return ONLY valid JSON with no markdown or backticks:
|
|
51
|
+
{
|
|
52
|
+
"severity": "OK" | "MINOR" | "MAJOR",
|
|
53
|
+
"verdict": "string",
|
|
54
|
+
"issues": [
|
|
55
|
+
{
|
|
56
|
+
"id": number,
|
|
57
|
+
"title": "string",
|
|
58
|
+
"description": "string",
|
|
59
|
+
"area": "string"
|
|
60
|
+
}
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
Each issue should be atomic and specific (e.g. 'Button shifted 8px right', not 'Layout problems'). area should be a CSS selector or component name if identifiable.
|
|
64
|
+
If the screenshots look identical, return severity "OK" with an empty issues array.`,
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
});
|
|
70
|
+
const text = response.content[0].type === "text" ? response.content[0].text : "";
|
|
71
|
+
console.log(chalk.green("✔") + " Analyzing with Claude...");
|
|
72
|
+
const result = JSON.parse(text);
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
export async function getFixSuggestions(issues) {
|
|
76
|
+
const client = new Anthropic();
|
|
77
|
+
const response = await client.messages.create({
|
|
78
|
+
model: "claude-sonnet-4-20250514",
|
|
79
|
+
max_tokens: 4096,
|
|
80
|
+
messages: [
|
|
81
|
+
{
|
|
82
|
+
role: "user",
|
|
83
|
+
content: `You are a senior frontend developer. For each of the following visual issues found in a UI screenshot comparison, provide a specific, actionable CSS or code fix suggestion. Be concrete — reference property names, values, and selectors where possible.
|
|
84
|
+
|
|
85
|
+
Issues:
|
|
86
|
+
${JSON.stringify(issues, null, 2)}
|
|
87
|
+
|
|
88
|
+
Return ONLY valid JSON with no markdown:
|
|
89
|
+
{
|
|
90
|
+
"fixes": [
|
|
91
|
+
{
|
|
92
|
+
"id": number,
|
|
93
|
+
"title": "string",
|
|
94
|
+
"suggestion": "string"
|
|
95
|
+
}
|
|
96
|
+
]
|
|
97
|
+
}`,
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
});
|
|
101
|
+
const text = response.content[0].type === "text" ? response.content[0].text : "";
|
|
102
|
+
const result = JSON.parse(text);
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=analyze.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analyze.js","sourceRoot":"","sources":["../src/analyze.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,mBAAmB,CAAC;AAC1C,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,KAAK,MAAM,OAAO,CAAC;AA2B1B,SAAS,aAAa,CAAC,QAAgB;IACrC,OAAO,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;AACtD,CAAC;AAED,SAAS,YAAY,CAAC,QAAgB;IACpC,MAAM,GAAG,GAAG,QAAQ,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC;IACpD,IAAI,GAAG,KAAK,KAAK,IAAI,GAAG,KAAK,MAAM;QAAE,OAAO,YAAY,CAAC;IACzD,IAAI,GAAG,KAAK,MAAM;QAAE,OAAO,YAAY,CAAC;IACxC,IAAI,GAAG,KAAK,KAAK;QAAE,OAAO,WAAW,CAAC;IACtC,OAAO,WAAW,CAAC;AACrB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,aAAqB,EACrB,YAAoB;IAEpB,MAAM,MAAM,GAAG,IAAI,SAAS,EAAE,CAAC;IAE/B,MAAM,SAAS,GAAG,aAAa,CAAC,aAAa,CAAC,CAAC;IAC/C,MAAM,SAAS,GAAG,aAAa,CAAC,YAAY,CAAC,CAAC;IAC9C,MAAM,YAAY,GAAG,YAAY,CAAC,aAAa,CAAC,CAAC;IACjD,MAAM,YAAY,GAAG,YAAY,CAAC,YAAY,CAAC,CAAC;IAEhD,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;QAC5C,KAAK,EAAE,0BAA0B;QACjC,UAAU,EAAE,IAAI;QAChB,MAAM,EAAE,wDAAwD;QAChE,QAAQ,EAAE;YACR;gBACE,IAAI,EAAE,MAAM;gBACZ,OAAO,EAAE;oBACP;wBACE,IAAI,EAAE,OAAO;wBACb,MAAM,EAAE;4BACN,IAAI,EAAE,QAAQ;4BACd,UAAU,EAAE,YAAY;4BACxB,IAAI,EAAE,SAAS;yBAChB;qBACF;oBACD;wBACE,IAAI,EAAE,OAAO;wBACb,MAAM,EAAE;4BACN,IAAI,EAAE,QAAQ;4BACd,UAAU,EAAE,YAAY;4BACxB,IAAI,EAAE,SAAS;yBAChB;qBACF;oBACD;wBACE,IAAI,EAAE,MAAM;wBACZ,IAAI,EAAE;;;;;;;;;;;;;;;oFAekE;qBACzE;iBACF;aACF;SACF;KACF,CAAC,CAAC;IAEH,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;IAEjF,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,2BAA2B,CAAC,CAAC;IAE5D,MAAM,MAAM,GAAmB,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAChD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,MAAe;IACrD,MAAM,MAAM,GAAG,IAAI,SAAS,EAAE,CAAC;IAE/B,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;QAC5C,KAAK,EAAE,0BAA0B;QACjC,UAAU,EAAE,IAAI;QAChB,QAAQ,EAAE;YACR;gBACE,IAAI,EAAE,MAAM;gBACZ,OAAO,EAAE;;;EAGf,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;;;;;;;;;;;EAW/B;aACK;SACF;KACF,CAAC,CAAC;IAEH,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;IAEjF,MAAM,MAAM,GAAc,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC3C,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function captureScreenshot(url: string, outputDir: string, width: number, height: number): Promise<string>;
|
package/dist/capture.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
async function ensureChromium() {
|
|
6
|
+
const { chromium } = await import("playwright");
|
|
7
|
+
const execPath = chromium.executablePath();
|
|
8
|
+
if (!fs.existsSync(execPath)) {
|
|
9
|
+
console.log(chalk.yellow("⚙") + " Chromium not found. Installing via Playwright...");
|
|
10
|
+
try {
|
|
11
|
+
execSync("npx playwright install chromium", { stdio: "inherit" });
|
|
12
|
+
console.log(chalk.green("✔") + " Chromium installed successfully.");
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
console.error(chalk.red("Failed to install Chromium. Try running manually:\n") +
|
|
16
|
+
chalk.cyan(" npx playwright install chromium"));
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export async function captureScreenshot(url, outputDir, width, height) {
|
|
22
|
+
await ensureChromium();
|
|
23
|
+
const { chromium } = await import("playwright");
|
|
24
|
+
const screenshotPath = path.join(outputDir, "captured.png");
|
|
25
|
+
const browser = await chromium.launch({ headless: true });
|
|
26
|
+
try {
|
|
27
|
+
const context = await browser.newContext({
|
|
28
|
+
viewport: { width, height },
|
|
29
|
+
ignoreHTTPSErrors: true,
|
|
30
|
+
});
|
|
31
|
+
const page = await context.newPage();
|
|
32
|
+
await page.goto(url, { waitUntil: "networkidle", timeout: 30000 });
|
|
33
|
+
await page.screenshot({ path: screenshotPath, fullPage: false });
|
|
34
|
+
console.log(chalk.green("✔") + ` Captured screenshot from ${chalk.cyan(url)}`);
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
await browser.close();
|
|
38
|
+
}
|
|
39
|
+
return screenshotPath;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=capture.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capture.js","sourceRoot":"","sources":["../src/capture.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,KAAK,UAAU,cAAc;IAC3B,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,CAAC;IAChD,MAAM,QAAQ,GAAG,QAAQ,CAAC,cAAc,EAAE,CAAC;IAE3C,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC7B,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,mDAAmD,CAAC,CAAC;QACrF,IAAI,CAAC;YACH,QAAQ,CAAC,iCAAiC,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;YAClE,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,mCAAmC,CAAC,CAAC;QACtE,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,CAAC,KAAK,CACX,KAAK,CAAC,GAAG,CAAC,qDAAqD,CAAC;gBAC9D,KAAK,CAAC,IAAI,CAAC,mCAAmC,CAAC,CAClD,CAAC;YACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;IACH,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,GAAW,EACX,SAAiB,EACjB,KAAa,EACb,MAAc;IAEd,MAAM,cAAc,EAAE,CAAC;IAEvB,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,CAAC;IAChD,MAAM,cAAc,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,cAAc,CAAC,CAAC;IAE5D,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;IAC1D,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC;YACvC,QAAQ,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE;YAC3B,iBAAiB,EAAE,IAAI;SACxB,CAAC,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC;QAErC,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,aAAa,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;QACnE,MAAM,IAAI,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;QAEjE,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,6BAA6B,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACjF,CAAC;YAAS,CAAC;QACT,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;IACxB,CAAC;IAED,OAAO,cAAc,CAAC;AACxB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function readClipboardImage(outputDir: string): Promise<string>;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { input } from "@inquirer/prompts";
|
|
6
|
+
function hasCommand(name) {
|
|
7
|
+
try {
|
|
8
|
+
execSync(`which ${name}`, { stdio: "ignore" });
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function getLinuxClipboardTools() {
|
|
16
|
+
const tools = [];
|
|
17
|
+
// wl-paste is preferred on Wayland but xclip works too (via XWayland)
|
|
18
|
+
if (hasCommand("wl-paste")) {
|
|
19
|
+
tools.push({ cmd: "wl-paste --type image/png", name: "wl-paste" });
|
|
20
|
+
}
|
|
21
|
+
if (hasCommand("xclip")) {
|
|
22
|
+
tools.push({ cmd: "xclip -selection clipboard -target image/png -o", name: "xclip" });
|
|
23
|
+
}
|
|
24
|
+
if (hasCommand("xsel")) {
|
|
25
|
+
tools.push({ cmd: "xsel --clipboard --output", name: "xsel" });
|
|
26
|
+
}
|
|
27
|
+
return tools;
|
|
28
|
+
}
|
|
29
|
+
function tryReadClipboard(tool) {
|
|
30
|
+
try {
|
|
31
|
+
const buf = execSync(tool.cmd, {
|
|
32
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
33
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
34
|
+
});
|
|
35
|
+
if (buf.length > 0)
|
|
36
|
+
return buf;
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export async function readClipboardImage(outputDir) {
|
|
44
|
+
let tools = [];
|
|
45
|
+
if (process.platform === "darwin") {
|
|
46
|
+
if (hasCommand("pngpaste")) {
|
|
47
|
+
tools.push({ cmd: "pngpaste -", name: "pngpaste" });
|
|
48
|
+
}
|
|
49
|
+
if (hasCommand("osascript")) {
|
|
50
|
+
// pbpaste doesn't support images, but osascript can write clipboard image to a file
|
|
51
|
+
const tmpPath = path.join(outputDir, "clipboard-ref.png");
|
|
52
|
+
tools.push({
|
|
53
|
+
cmd: `osascript -e 'try' -e 'set imgData to the clipboard as «class PNGf»' -e 'set fp to open for access POSIX file "${tmpPath}" with write permission' -e 'write imgData to fp' -e 'close access fp' -e 'end try'`,
|
|
54
|
+
name: "osascript",
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else if (process.platform === "linux") {
|
|
59
|
+
tools = getLinuxClipboardTools();
|
|
60
|
+
}
|
|
61
|
+
else if (process.platform === "win32") {
|
|
62
|
+
tools.push({
|
|
63
|
+
cmd: `powershell -command "Add-Type -AssemblyName System.Windows.Forms; $img = [System.Windows.Forms.Clipboard]::GetImage(); if ($img) { $img.Save([Console]::OpenStandardOutput(), [System.Drawing.Imaging.ImageFormat]::Png) } else { exit 1 }"`,
|
|
64
|
+
name: "powershell",
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
if (tools.length === 0) {
|
|
68
|
+
console.error(chalk.red("No clipboard tool found.\n") +
|
|
69
|
+
chalk.dim(" Install one of: wl-clipboard, xclip, xsel\n") +
|
|
70
|
+
chalk.cyan(" sudo pacman -S xclip # Arch\n") +
|
|
71
|
+
chalk.cyan(" sudo apt install xclip # Debian/Ubuntu"));
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
// Try each available tool until one returns image data
|
|
75
|
+
for (const tool of tools) {
|
|
76
|
+
const buf = tryReadClipboard(tool);
|
|
77
|
+
if (buf) {
|
|
78
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
79
|
+
const outPath = path.join(outputDir, "clipboard-ref.png");
|
|
80
|
+
fs.writeFileSync(outPath, buf);
|
|
81
|
+
console.log(chalk.green("✔") +
|
|
82
|
+
` Read reference image from clipboard via ${tool.name} (${(buf.length / 1024).toFixed(0)} KB)`);
|
|
83
|
+
return outPath;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
console.log(chalk.yellow("⚙") + " No image found in clipboard.");
|
|
87
|
+
console.log(chalk.dim(" You can drag & drop an image file into this terminal, or paste its path.\n"));
|
|
88
|
+
const filePath = await input({
|
|
89
|
+
message: "Path to reference image (drag file here or paste path):",
|
|
90
|
+
});
|
|
91
|
+
const cleaned = filePath.trim().replace(/^['"]|['"]$/g, "");
|
|
92
|
+
if (!cleaned || !fs.existsSync(cleaned)) {
|
|
93
|
+
console.error(chalk.red("File not found: ") + chalk.dim(cleaned || "(empty)"));
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
const ext = path.extname(cleaned).toLowerCase();
|
|
97
|
+
if (![".png", ".jpg", ".jpeg", ".webp", ".bmp"].includes(ext)) {
|
|
98
|
+
console.error(chalk.red("Unsupported image format: ") + chalk.dim(ext));
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
102
|
+
const outPath = path.join(outputDir, "clipboard-ref" + ext);
|
|
103
|
+
fs.copyFileSync(cleaned, outPath);
|
|
104
|
+
const size = fs.statSync(outPath).size;
|
|
105
|
+
console.log(chalk.green("✔") +
|
|
106
|
+
` Using reference image: ${chalk.cyan(path.basename(cleaned))} (${(size / 1024).toFixed(0)} KB)`);
|
|
107
|
+
return outPath;
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=clipboard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clipboard.js","sourceRoot":"","sources":["../src/clipboard.ts"],"names":[],"mappings":"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"}
|
package/dist/diff.d.ts
ADDED
package/dist/diff.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createJimp } from "@jimp/core";
|
|
2
|
+
import png from "@jimp/js-png";
|
|
3
|
+
import jpeg from "@jimp/js-jpeg";
|
|
4
|
+
import * as resize from "@jimp/plugin-resize";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
const Jimp = createJimp({ formats: [png, jpeg], plugins: [resize.methods] });
|
|
8
|
+
export async function runDiff(referencePath, capturedPath, outputDir) {
|
|
9
|
+
const refImage = await Jimp.read(referencePath);
|
|
10
|
+
const capImage = await Jimp.read(capturedPath);
|
|
11
|
+
const width = capImage.width;
|
|
12
|
+
const height = capImage.height;
|
|
13
|
+
// Resize reference to match captured dimensions if they differ
|
|
14
|
+
if (refImage.width !== width || refImage.height !== height) {
|
|
15
|
+
refImage.resize({ w: width, h: height });
|
|
16
|
+
}
|
|
17
|
+
const diffImage = new Jimp({ width, height });
|
|
18
|
+
const threshold = 0.1;
|
|
19
|
+
let diffPixels = 0;
|
|
20
|
+
const totalPixels = width * height;
|
|
21
|
+
for (let y = 0; y < height; y++) {
|
|
22
|
+
for (let x = 0; x < width; x++) {
|
|
23
|
+
const refColor = refImage.getPixelColor(x, y);
|
|
24
|
+
const capColor = capImage.getPixelColor(x, y);
|
|
25
|
+
if (refColor !== capColor) {
|
|
26
|
+
// Extract RGBA components
|
|
27
|
+
const rRef = (refColor >> 24) & 0xff;
|
|
28
|
+
const gRef = (refColor >> 16) & 0xff;
|
|
29
|
+
const bRef = (refColor >> 8) & 0xff;
|
|
30
|
+
const rCap = (capColor >> 24) & 0xff;
|
|
31
|
+
const gCap = (capColor >> 16) & 0xff;
|
|
32
|
+
const bCap = (capColor >> 8) & 0xff;
|
|
33
|
+
const maxDiff = Math.max(Math.abs(rRef - rCap), Math.abs(gRef - gCap), Math.abs(bRef - bCap));
|
|
34
|
+
if (maxDiff / 255 > threshold) {
|
|
35
|
+
// Highlight difference in red
|
|
36
|
+
diffImage.setPixelColor(0xff0000ff, x, y);
|
|
37
|
+
diffPixels++;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
// Below threshold — show faded original
|
|
41
|
+
const gray = Math.round((rCap + gCap + bCap) / 3);
|
|
42
|
+
diffImage.setPixelColor(((gray << 24) | (gray << 16) | (gray << 8) | 0x80) >>> 0, x, y);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
// Identical pixel — show faded
|
|
47
|
+
const r = (capColor >> 24) & 0xff;
|
|
48
|
+
const g = (capColor >> 16) & 0xff;
|
|
49
|
+
const b = (capColor >> 8) & 0xff;
|
|
50
|
+
const gray = Math.round((r + g + b) / 3);
|
|
51
|
+
diffImage.setPixelColor(((gray << 24) | (gray << 16) | (gray << 8) | 0x80) >>> 0, x, y);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const diffPath = path.join(outputDir, "diff.png");
|
|
56
|
+
await diffImage.write(diffPath);
|
|
57
|
+
const diffPercent = (diffPixels / totalPixels) * 100;
|
|
58
|
+
console.log(chalk.green("✔") +
|
|
59
|
+
` Running pixel diff... (${chalk.yellow(diffPercent.toFixed(1) + "%")} difference)`);
|
|
60
|
+
return { diffPath, diffPercent };
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=diff.js.map
|
package/dist/diff.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff.js","sourceRoot":"","sources":["../src/diff.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import https from "node:https";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import open from "open";
|
|
8
|
+
import { input, password } from "@inquirer/prompts";
|
|
9
|
+
import { captureScreenshot } from "./capture.js";
|
|
10
|
+
import { readClipboardImage } from "./clipboard.js";
|
|
11
|
+
import { runDiff } from "./diff.js";
|
|
12
|
+
import { analyzeScreenshots } from "./analyze.js";
|
|
13
|
+
import { runInteractive } from "./interactive.js";
|
|
14
|
+
import { generateReport } from "./report.js";
|
|
15
|
+
const program = new Command();
|
|
16
|
+
program
|
|
17
|
+
.name("visual-mirror")
|
|
18
|
+
.description("Pixel diffs tell you something changed. Visual Mirror tells you what it means.")
|
|
19
|
+
.version("1.0.0")
|
|
20
|
+
.option("--ref <path>", "Path to reference screenshot (PNG/JPG). If omitted, reads from clipboard")
|
|
21
|
+
.requiredOption("--url <url>", "URL to capture and compare")
|
|
22
|
+
.option("--width <number>", "Viewport width", "1280")
|
|
23
|
+
.option("--height <number>", "Viewport height", "720")
|
|
24
|
+
.option("--out <path>", "Output directory for report", "./visual-mirror-report");
|
|
25
|
+
program.parse();
|
|
26
|
+
const opts = program.opts();
|
|
27
|
+
async function ensureApiKey() {
|
|
28
|
+
if (process.env.ANTHROPIC_API_KEY)
|
|
29
|
+
return;
|
|
30
|
+
console.log(chalk.yellow("⚙") + " ANTHROPIC_API_KEY not found in your environment.");
|
|
31
|
+
console.log(chalk.dim(" Get one at https://console.anthropic.com\n"));
|
|
32
|
+
const key = await password({
|
|
33
|
+
message: "Enter your Anthropic API key:",
|
|
34
|
+
mask: "*",
|
|
35
|
+
});
|
|
36
|
+
const trimmed = key.trim();
|
|
37
|
+
if (!trimmed) {
|
|
38
|
+
console.error(chalk.red("No API key provided. Exiting."));
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
process.env.ANTHROPIC_API_KEY = trimmed;
|
|
42
|
+
const shell = path.basename(process.env.SHELL || "bash");
|
|
43
|
+
const rcFile = shell === "zsh" ? "~/.zshrc" : shell === "fish" ? "~/.config/fish/config.fish" : "~/.bashrc";
|
|
44
|
+
const exportCmd = shell === "fish"
|
|
45
|
+
? `set -Ux ANTHROPIC_API_KEY "${trimmed}"`
|
|
46
|
+
: `export ANTHROPIC_API_KEY="${trimmed}"`;
|
|
47
|
+
console.log();
|
|
48
|
+
console.log(chalk.green("✔") + " API key set for this session.");
|
|
49
|
+
console.log(chalk.dim(" To persist it, add this to ") + chalk.cyan(rcFile) + chalk.dim(":"));
|
|
50
|
+
console.log(chalk.dim(` ${exportCmd}`));
|
|
51
|
+
console.log();
|
|
52
|
+
}
|
|
53
|
+
async function tryUrl(url) {
|
|
54
|
+
try {
|
|
55
|
+
if (url.startsWith("https://")) {
|
|
56
|
+
// Use node:https to allow self-signed certs (common for local dev servers)
|
|
57
|
+
return await new Promise((resolve) => {
|
|
58
|
+
const req = https.get(url, { rejectUnauthorized: false, timeout: 5000 }, (res) => {
|
|
59
|
+
res.resume();
|
|
60
|
+
resolve(true);
|
|
61
|
+
});
|
|
62
|
+
req.on("error", () => resolve(false));
|
|
63
|
+
req.on("timeout", () => {
|
|
64
|
+
req.destroy();
|
|
65
|
+
resolve(false);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
await fetch(url, { signal: AbortSignal.timeout(5000) });
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async function resolveUrl(url) {
|
|
77
|
+
if (await tryUrl(url))
|
|
78
|
+
return url;
|
|
79
|
+
// Try alternative ports if the URL has a port
|
|
80
|
+
let parsed;
|
|
81
|
+
try {
|
|
82
|
+
parsed = new URL(url);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return url; // Let it fail later with a proper error
|
|
86
|
+
}
|
|
87
|
+
if (parsed.port) {
|
|
88
|
+
const basePort = parseInt(parsed.port, 10);
|
|
89
|
+
const alternatives = [basePort + 1, basePort - 1, 8080, 3000, 5173, 4200, 8000];
|
|
90
|
+
const uniquePorts = [
|
|
91
|
+
...new Set(alternatives.filter((p) => p !== basePort && p > 0 && p <= 65535)),
|
|
92
|
+
];
|
|
93
|
+
console.log(chalk.yellow("⚙") + ` Could not reach ${chalk.cyan(url)}. Trying nearby ports...`);
|
|
94
|
+
for (const port of uniquePorts) {
|
|
95
|
+
const alt = new URL(url);
|
|
96
|
+
alt.port = String(port);
|
|
97
|
+
const altUrl = alt.toString();
|
|
98
|
+
if (await tryUrl(altUrl)) {
|
|
99
|
+
console.log(chalk.green("✔") + ` Found live server at ${chalk.cyan(altUrl)}`);
|
|
100
|
+
const answer = await input({
|
|
101
|
+
message: `Use ${altUrl} instead? (Y/n)`,
|
|
102
|
+
default: "Y",
|
|
103
|
+
});
|
|
104
|
+
if (answer.trim().toLowerCase() !== "n") {
|
|
105
|
+
return altUrl;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Nothing worked — give the user instructions
|
|
111
|
+
console.error(chalk.red(`\nCould not reach ${url}`));
|
|
112
|
+
console.error(chalk.dim(" Make sure your app is running. For example:\n"));
|
|
113
|
+
console.error(chalk.dim(" # React / Vite / Next.js"));
|
|
114
|
+
console.error(chalk.cyan(" npm run dev\n"));
|
|
115
|
+
console.error(chalk.dim(" # Quick static file server"));
|
|
116
|
+
console.error(chalk.cyan(" npx serve ./dist -p 3000\n"));
|
|
117
|
+
console.error(chalk.dim(" # Python"));
|
|
118
|
+
console.error(chalk.cyan(" python -m http.server 3000\n"));
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
async function main() {
|
|
122
|
+
// Step 0: Ensure API key
|
|
123
|
+
await ensureApiKey();
|
|
124
|
+
const width = parseInt(opts.width, 10);
|
|
125
|
+
const height = parseInt(opts.height, 10);
|
|
126
|
+
const outputDir = path.resolve(opts.out);
|
|
127
|
+
// Resolve reference: from --ref flag or clipboard
|
|
128
|
+
let refPath;
|
|
129
|
+
if (opts.ref) {
|
|
130
|
+
refPath = path.resolve(opts.ref);
|
|
131
|
+
if (!fs.existsSync(refPath)) {
|
|
132
|
+
console.error(chalk.red(`Reference file not found: ${refPath}`));
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
console.log(chalk.yellow("⚙") + " No --ref provided. Reading reference image from clipboard...");
|
|
138
|
+
refPath = await readClipboardImage(outputDir);
|
|
139
|
+
}
|
|
140
|
+
// Ensure output directory exists
|
|
141
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
142
|
+
// Step 1: Resolve URL (check reachability, try alt ports)
|
|
143
|
+
const resolvedUrl = await resolveUrl(opts.url);
|
|
144
|
+
// Step 2: Capture screenshot
|
|
145
|
+
let capturedPath;
|
|
146
|
+
try {
|
|
147
|
+
capturedPath = await captureScreenshot(resolvedUrl, outputDir, width, height);
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
console.error(chalk.red(`Could not capture screenshot from ${resolvedUrl}`));
|
|
151
|
+
console.error(chalk.dim(String(err)));
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
// Step 3: Pixel diff
|
|
155
|
+
const { diffPath, diffPercent } = await runDiff(refPath, capturedPath, outputDir);
|
|
156
|
+
// Step 4: Claude Vision analysis
|
|
157
|
+
let analysis;
|
|
158
|
+
try {
|
|
159
|
+
analysis = await analyzeScreenshots(refPath, capturedPath);
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
console.error(chalk.red("Claude API error:"), err);
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
// Step 5: Interactive selection + fix suggestions
|
|
166
|
+
const fixes = await runInteractive(analysis);
|
|
167
|
+
// Step 6: Generate report
|
|
168
|
+
const reportPath = generateReport(resolvedUrl, refPath, capturedPath, diffPath, diffPercent, analysis, fixes, outputDir);
|
|
169
|
+
console.log(chalk.green("✔") + ` Report saved to ${chalk.cyan(reportPath)}`);
|
|
170
|
+
console.log(" Opening report...");
|
|
171
|
+
await open(reportPath);
|
|
172
|
+
}
|
|
173
|
+
main().catch((err) => {
|
|
174
|
+
if (err instanceof Error && err.name === "ExitPromptError") {
|
|
175
|
+
console.log("\nAborted.");
|
|
176
|
+
process.exit(0);
|
|
177
|
+
}
|
|
178
|
+
console.error(chalk.red("Unexpected error:"), err);
|
|
179
|
+
process.exit(1);
|
|
180
|
+
});
|
|
181
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { input } from "@inquirer/prompts";
|
|
3
|
+
import { getFixSuggestions } from "./analyze.js";
|
|
4
|
+
function severityColor(severity) {
|
|
5
|
+
switch (severity) {
|
|
6
|
+
case "OK":
|
|
7
|
+
return chalk.green(severity);
|
|
8
|
+
case "MINOR":
|
|
9
|
+
return chalk.yellow(severity);
|
|
10
|
+
case "MAJOR":
|
|
11
|
+
return chalk.red(severity);
|
|
12
|
+
default:
|
|
13
|
+
return severity;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function displayResults(analysis) {
|
|
17
|
+
console.log();
|
|
18
|
+
console.log(` Severity: ${severityColor(analysis.severity)}`);
|
|
19
|
+
console.log();
|
|
20
|
+
if (analysis.issues.length === 0) {
|
|
21
|
+
console.log(chalk.green(" No issues found — screenshots match!"));
|
|
22
|
+
console.log();
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
console.log(" Issues found:");
|
|
26
|
+
for (const issue of analysis.issues) {
|
|
27
|
+
const id = chalk.white(String(issue.id).padStart(3));
|
|
28
|
+
const title = chalk.white(issue.title.padEnd(55));
|
|
29
|
+
const area = chalk.dim(issue.area);
|
|
30
|
+
console.log(` ${id} ${title} ${area}`);
|
|
31
|
+
}
|
|
32
|
+
console.log();
|
|
33
|
+
}
|
|
34
|
+
function parseSelection(input, maxId) {
|
|
35
|
+
const trimmed = input.trim();
|
|
36
|
+
if (trimmed === "*") {
|
|
37
|
+
return Array.from({ length: maxId }, (_, i) => i + 1);
|
|
38
|
+
}
|
|
39
|
+
const parts = trimmed.split(",").map((s) => s.trim());
|
|
40
|
+
const ids = [];
|
|
41
|
+
for (const part of parts) {
|
|
42
|
+
const num = parseInt(part, 10);
|
|
43
|
+
if (isNaN(num) || num < 1 || num > maxId) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
ids.push(num);
|
|
47
|
+
}
|
|
48
|
+
return ids.length > 0 ? [...new Set(ids)] : null;
|
|
49
|
+
}
|
|
50
|
+
export async function runInteractive(analysis) {
|
|
51
|
+
displayResults(analysis);
|
|
52
|
+
if (analysis.issues.length === 0) {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
let selectedIds = null;
|
|
56
|
+
while (selectedIds === null) {
|
|
57
|
+
const answer = await input({
|
|
58
|
+
message: chalk.cyan("==>") +
|
|
59
|
+
" Select issues to get fix suggestions for (e.g. 1,2 or * for all)\n" +
|
|
60
|
+
chalk.cyan("==>"),
|
|
61
|
+
});
|
|
62
|
+
selectedIds = parseSelection(answer, analysis.issues.length);
|
|
63
|
+
if (selectedIds === null) {
|
|
64
|
+
console.log(chalk.red(" Invalid selection. Enter issue numbers separated by commas, or * for all."));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const selectedIssues = analysis.issues.filter((issue) => selectedIds.includes(issue.id));
|
|
68
|
+
console.log();
|
|
69
|
+
console.log(chalk.dim(" Fetching fix suggestions from Claude..."));
|
|
70
|
+
const fixResult = await getFixSuggestions(selectedIssues);
|
|
71
|
+
console.log();
|
|
72
|
+
console.log(" Fix suggestions:");
|
|
73
|
+
console.log();
|
|
74
|
+
for (const fix of fixResult.fixes) {
|
|
75
|
+
console.log(chalk.white(` [${fix.id}] ${fix.title}`));
|
|
76
|
+
const lines = fix.suggestion.split("\n");
|
|
77
|
+
for (const line of lines) {
|
|
78
|
+
console.log(chalk.dim(` → ${line}`));
|
|
79
|
+
}
|
|
80
|
+
console.log();
|
|
81
|
+
}
|
|
82
|
+
return fixResult.fixes;
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=interactive.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"interactive.js","sourceRoot":"","sources":["../src/interactive.ts"],"names":[],"mappings":"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"}
|
package/dist/report.d.ts
ADDED
package/dist/report.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
function imageToDataUrl(filePath) {
|
|
4
|
+
const data = fs.readFileSync(filePath).toString("base64");
|
|
5
|
+
const ext = filePath.toLowerCase().split(".").pop();
|
|
6
|
+
const mime = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : "image/png";
|
|
7
|
+
return `data:${mime};base64,${data}`;
|
|
8
|
+
}
|
|
9
|
+
function severityBadge(severity) {
|
|
10
|
+
const colors = {
|
|
11
|
+
OK: "#22c55e",
|
|
12
|
+
MINOR: "#eab308",
|
|
13
|
+
MAJOR: "#ef4444",
|
|
14
|
+
};
|
|
15
|
+
const color = colors[severity] || "#6b7280";
|
|
16
|
+
return `<span style="background:${color};color:#000;padding:4px 12px;border-radius:4px;font-weight:bold;">${severity}</span>`;
|
|
17
|
+
}
|
|
18
|
+
export function generateReport(url, referencePath, capturedPath, diffPath, diffPercent, analysis, fixes, outputDir) {
|
|
19
|
+
const timestamp = new Date().toISOString();
|
|
20
|
+
const refDataUrl = imageToDataUrl(referencePath);
|
|
21
|
+
const capDataUrl = imageToDataUrl(capturedPath);
|
|
22
|
+
const diffDataUrl = imageToDataUrl(diffPath);
|
|
23
|
+
const issuesHtml = analysis.issues
|
|
24
|
+
.map((issue) => `
|
|
25
|
+
<div style="background:#1e1e2e;padding:12px 16px;border-radius:6px;margin-bottom:8px;">
|
|
26
|
+
<strong style="color:#cdd6f4;">#${issue.id} ${issue.title}</strong>
|
|
27
|
+
<span style="color:#6c7086;margin-left:12px;">${issue.area}</span>
|
|
28
|
+
<p style="color:#a6adc8;margin:6px 0 0;">${issue.description}</p>
|
|
29
|
+
</div>`)
|
|
30
|
+
.join("\n");
|
|
31
|
+
let fixesHtml;
|
|
32
|
+
if (fixes.length === 0) {
|
|
33
|
+
fixesHtml = `<p style="color:#6c7086;font-style:italic;">No fixes requested.</p>`;
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
fixesHtml = fixes
|
|
37
|
+
.map((fix) => `
|
|
38
|
+
<div style="background:#1e1e2e;padding:12px 16px;border-radius:6px;margin-bottom:8px;">
|
|
39
|
+
<strong style="color:#cdd6f4;">[${fix.id}] ${fix.title}</strong>
|
|
40
|
+
<pre style="color:#a6e3a1;margin:8px 0 0;white-space:pre-wrap;font-size:13px;">${fix.suggestion}</pre>
|
|
41
|
+
</div>`)
|
|
42
|
+
.join("\n");
|
|
43
|
+
}
|
|
44
|
+
const html = `<!DOCTYPE html>
|
|
45
|
+
<html lang="en">
|
|
46
|
+
<head>
|
|
47
|
+
<meta charset="UTF-8">
|
|
48
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
49
|
+
<title>Visual Mirror Report</title>
|
|
50
|
+
<style>
|
|
51
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
52
|
+
body {
|
|
53
|
+
background: #11111b;
|
|
54
|
+
color: #cdd6f4;
|
|
55
|
+
font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
|
|
56
|
+
padding: 32px;
|
|
57
|
+
line-height: 1.6;
|
|
58
|
+
}
|
|
59
|
+
h1 { color: #cba6f7; font-size: 24px; margin-bottom: 8px; }
|
|
60
|
+
h2 { color: #89b4fa; font-size: 18px; margin: 32px 0 16px; }
|
|
61
|
+
.header-meta {
|
|
62
|
+
color: #6c7086;
|
|
63
|
+
font-size: 13px;
|
|
64
|
+
margin-bottom: 24px;
|
|
65
|
+
}
|
|
66
|
+
.header-meta span { margin-right: 24px; }
|
|
67
|
+
.image-grid {
|
|
68
|
+
display: grid;
|
|
69
|
+
grid-template-columns: 1fr 1fr 1fr;
|
|
70
|
+
gap: 16px;
|
|
71
|
+
margin-bottom: 32px;
|
|
72
|
+
}
|
|
73
|
+
.image-grid div {
|
|
74
|
+
text-align: center;
|
|
75
|
+
}
|
|
76
|
+
.image-grid img {
|
|
77
|
+
width: 100%;
|
|
78
|
+
border: 1px solid #313244;
|
|
79
|
+
border-radius: 6px;
|
|
80
|
+
}
|
|
81
|
+
.image-grid p {
|
|
82
|
+
color: #6c7086;
|
|
83
|
+
font-size: 12px;
|
|
84
|
+
margin-top: 8px;
|
|
85
|
+
}
|
|
86
|
+
</style>
|
|
87
|
+
</head>
|
|
88
|
+
<body>
|
|
89
|
+
<h1>Visual Mirror Report</h1>
|
|
90
|
+
<div class="header-meta">
|
|
91
|
+
<span>URL: ${url}</span>
|
|
92
|
+
<span>Time: ${timestamp}</span>
|
|
93
|
+
<span>Diff: ${diffPercent.toFixed(1)}%</span>
|
|
94
|
+
<span>${severityBadge(analysis.severity)}</span>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<h2>Screenshots</h2>
|
|
98
|
+
<div class="image-grid">
|
|
99
|
+
<div>
|
|
100
|
+
<img src="${refDataUrl}" alt="Reference">
|
|
101
|
+
<p>Reference</p>
|
|
102
|
+
</div>
|
|
103
|
+
<div>
|
|
104
|
+
<img src="${capDataUrl}" alt="Captured">
|
|
105
|
+
<p>Captured</p>
|
|
106
|
+
</div>
|
|
107
|
+
<div>
|
|
108
|
+
<img src="${diffDataUrl}" alt="Diff">
|
|
109
|
+
<p>Diff</p>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<h2>Issues (${analysis.issues.length})</h2>
|
|
114
|
+
${issuesHtml || '<p style="color:#a6e3a1;">No issues detected.</p>'}
|
|
115
|
+
|
|
116
|
+
<h2>Fix Suggestions</h2>
|
|
117
|
+
${fixesHtml}
|
|
118
|
+
|
|
119
|
+
<p style="color:#45475a;margin-top:48px;font-size:11px;">Generated by visual-mirror</p>
|
|
120
|
+
</body>
|
|
121
|
+
</html>`;
|
|
122
|
+
const reportPath = path.join(outputDir, "index.html");
|
|
123
|
+
fs.writeFileSync(reportPath, html, "utf-8");
|
|
124
|
+
return reportPath;
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=report.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"report.js","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAG7B,SAAS,cAAc,CAAC,QAAgB;IACtC,MAAM,IAAI,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAC1D,MAAM,GAAG,GAAG,QAAQ,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC;IACpD,MAAM,IAAI,GAAG,GAAG,KAAK,KAAK,IAAI,GAAG,KAAK,MAAM,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,WAAW,CAAC;IAC1E,OAAO,QAAQ,IAAI,WAAW,IAAI,EAAE,CAAC;AACvC,CAAC;AAED,SAAS,aAAa,CAAC,QAAgB;IACrC,MAAM,MAAM,GAA2B;QACrC,EAAE,EAAE,SAAS;QACb,KAAK,EAAE,SAAS;QAChB,KAAK,EAAE,SAAS;KACjB,CAAC;IACF,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,IAAI,SAAS,CAAC;IAC5C,OAAO,2BAA2B,KAAK,qEAAqE,QAAQ,SAAS,CAAC;AAChI,CAAC;AAED,MAAM,UAAU,cAAc,CAC5B,GAAW,EACX,aAAqB,EACrB,YAAoB,EACpB,QAAgB,EAChB,WAAmB,EACnB,QAAwB,EACxB,KAAY,EACZ,SAAiB;IAEjB,MAAM,SAAS,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC3C,MAAM,UAAU,GAAG,cAAc,CAAC,aAAa,CAAC,CAAC;IACjD,MAAM,UAAU,GAAG,cAAc,CAAC,YAAY,CAAC,CAAC;IAChD,MAAM,WAAW,GAAG,cAAc,CAAC,QAAQ,CAAC,CAAC;IAE7C,MAAM,UAAU,GAAG,QAAQ,CAAC,MAAM;SAC/B,GAAG,CACF,CAAC,KAAK,EAAE,EAAE,CAAC;;0CAEyB,KAAK,CAAC,EAAE,IAAI,KAAK,CAAC,KAAK;wDACT,KAAK,CAAC,IAAI;mDACf,KAAK,CAAC,WAAW;aACvD,CACR;SACA,IAAI,CAAC,IAAI,CAAC,CAAC;IAEd,IAAI,SAAiB,CAAC;IACtB,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,SAAS,GAAG,qEAAqE,CAAC;IACpF,CAAC;SAAM,CAAC;QACN,SAAS,GAAG,KAAK;aACd,GAAG,CACF,CAAC,GAAG,EAAE,EAAE,CAAC;;0CAEyB,GAAG,CAAC,EAAE,KAAK,GAAG,CAAC,KAAK;yFAC2B,GAAG,CAAC,UAAU;aAC1F,CACN;aACA,IAAI,CAAC,IAAI,CAAC,CAAC;IAChB,CAAC;IAED,MAAM,IAAI,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA+CE,GAAG;kBACF,SAAS;kBACT,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC;YAC5B,aAAa,CAAC,QAAQ,CAAC,QAAQ,CAAC;;;;;;kBAM1B,UAAU;;;;kBAIV,UAAU;;;;kBAIV,WAAW;;;;;gBAKb,QAAQ,CAAC,MAAM,CAAC,MAAM;IAClC,UAAU,IAAI,mDAAmD;;;IAGjE,SAAS;;;;QAIL,CAAC;IAEP,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;IACtD,EAAE,CAAC,aAAa,CAAC,UAAU,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;IAC5C,OAAO,UAAU,CAAC;AACpB,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "visual-mirror",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "Pixel diffs tell you something changed. Visual Mirror tells you what it means.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"author": {
|
|
8
|
+
"email": "kaweh.taher.1989@gmail.com",
|
|
9
|
+
"name": "Kaveh Taher",
|
|
10
|
+
"url": "https://github.com/kawehtaher"
|
|
11
|
+
},
|
|
12
|
+
"bin": {
|
|
13
|
+
"visual-mirror": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc",
|
|
20
|
+
"start": "node dist/index.js",
|
|
21
|
+
"lint": "eslint src/",
|
|
22
|
+
"lint:fix": "eslint src/ --fix",
|
|
23
|
+
"format": "prettier --write 'src/**/*.ts'",
|
|
24
|
+
"format:check": "prettier --check 'src/**/*.ts'",
|
|
25
|
+
"typecheck": "tsc --noEmit",
|
|
26
|
+
"check": "npm run typecheck && npm run lint && npm run format:check",
|
|
27
|
+
"prepublishOnly": "npm run build"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"visual-regression",
|
|
31
|
+
"screenshot",
|
|
32
|
+
"diff",
|
|
33
|
+
"testing",
|
|
34
|
+
"ai",
|
|
35
|
+
"claude"
|
|
36
|
+
],
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@anthropic-ai/sdk": "^0.39.0",
|
|
40
|
+
"chalk": "^5.4.1",
|
|
41
|
+
"commander": "^13.1.0",
|
|
42
|
+
"inquirer": "^12.6.0",
|
|
43
|
+
"jimp": "^1.6.0",
|
|
44
|
+
"open": "^10.1.0",
|
|
45
|
+
"playwright": "^1.52.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@eslint/js": "^10.0.1",
|
|
49
|
+
"@types/node": "^22.15.2",
|
|
50
|
+
"eslint": "^10.2.0",
|
|
51
|
+
"prettier": "^3.8.1",
|
|
52
|
+
"typescript": "^5.8.3",
|
|
53
|
+
"typescript-eslint": "^8.58.0"
|
|
54
|
+
}
|
|
55
|
+
}
|