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 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
+ [![npm version](https://img.shields.io/npm/v/visual-mirror.svg)](https://www.npmjs.com/package/visual-mirror)
6
+ [![license](https://img.shields.io/npm/l/visual-mirror.svg)](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>;
@@ -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":"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"}
@@ -0,0 +1 @@
1
+ export declare function captureScreenshot(url: string, outputDir: string, width: number, height: number): Promise<string>;
@@ -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":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAO1C,SAAS,UAAU,CAAC,IAAY;IAC9B,IAAI,CAAC;QACH,QAAQ,CAAC,SAAS,IAAI,EAAE,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC;QAC/C,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,SAAS,sBAAsB;IAC7B,MAAM,KAAK,GAAoB,EAAE,CAAC;IAElC,sEAAsE;IACtE,IAAI,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC3B,KAAK,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,2BAA2B,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC;IACrE,CAAC;IACD,IAAI,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QACxB,KAAK,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,iDAAiD,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;IACxF,CAAC;IACD,IAAI,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;QACvB,KAAK,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,2BAA2B,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;IACjE,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,gBAAgB,CAAC,IAAmB;IAC3C,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,QAAQ,CAAC,IAAI,CAAC,GAAG,EAAE;YAC7B,SAAS,EAAE,EAAE,GAAG,IAAI,GAAG,IAAI;YAC3B,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC;SAClC,CAAC,CAAC;QACH,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,GAAG,CAAC;QAC/B,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,SAAiB;IACxD,IAAI,KAAK,GAAoB,EAAE,CAAC;IAEhC,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAClC,IAAI,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;YAC3B,KAAK,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,YAAY,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC;QACtD,CAAC;QACD,IAAI,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;YAC5B,oFAAoF;YACpF,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,mBAAmB,CAAC,CAAC;YAC1D,KAAK,CAAC,IAAI,CAAC;gBACT,GAAG,EAAE,kHAAkH,OAAO,qFAAqF;gBACnN,IAAI,EAAE,WAAW;aAClB,CAAC,CAAC;QACL,CAAC;IACH,CAAC;SAAM,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QACxC,KAAK,GAAG,sBAAsB,EAAE,CAAC;IACnC,CAAC;SAAM,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QACxC,KAAK,CAAC,IAAI,CAAC;YACT,GAAG,EAAE,6OAA6O;YAClP,IAAI,EAAE,YAAY;SACnB,CAAC,CAAC;IACL,CAAC;IAED,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO,CAAC,KAAK,CACX,KAAK,CAAC,GAAG,CAAC,4BAA4B,CAAC;YACrC,KAAK,CAAC,GAAG,CAAC,+CAA+C,CAAC;YAC1D,KAAK,CAAC,IAAI,CAAC,0CAA0C,CAAC;YACtD,KAAK,CAAC,IAAI,CAAC,iDAAiD,CAAC,CAChE,CAAC;QACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,uDAAuD;IACvD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,GAAG,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC;QACnC,IAAI,GAAG,EAAE,CAAC;YACR,EAAE,CAAC,SAAS,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YAC7C,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,mBAAmB,CAAC,CAAC;YAC1D,EAAE,CAAC,aAAa,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;YAE/B,OAAO,CAAC,GAAG,CACT,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC;gBACd,4CAA4C,IAAI,CAAC,IAAI,KAAK,CAAC,GAAG,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CACjG,CAAC;YAEF,OAAO,OAAO,CAAC;QACjB,CAAC;IACH,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,+BAA+B,CAAC,CAAC;IACjE,OAAO,CAAC,GAAG,CACT,KAAK,CAAC,GAAG,CAAC,8EAA8E,CAAC,CAC1F,CAAC;IAEF,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC;QAC3B,OAAO,EAAE,yDAAyD;KACnE,CAAC,CAAC;IAEH,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC;IAC5D,IAAI,CAAC,OAAO,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QACxC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,kBAAkB,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,IAAI,SAAS,CAAC,CAAC,CAAC;QAC/E,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,WAAW,EAAE,CAAC;IAChD,IAAI,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QAC9D,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,4BAA4B,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;QACxE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,EAAE,CAAC,SAAS,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7C,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,eAAe,GAAG,GAAG,CAAC,CAAC;IAC5D,EAAE,CAAC,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAElC,MAAM,IAAI,GAAG,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC;IACvC,OAAO,CAAC,GAAG,CACT,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC;QACd,2BAA2B,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CACnG,CAAC;IAEF,OAAO,OAAO,CAAC;AACjB,CAAC"}
package/dist/diff.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ export type DiffResult = {
2
+ diffPath: string;
3
+ diffPercent: number;
4
+ };
5
+ export declare function runDiff(referencePath: string, capturedPath: string, outputDir: string): Promise<DiffResult>;
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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff.js","sourceRoot":"","sources":["../src/diff.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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,2 @@
1
+ import type { AnalysisResult, Fix } from "./analyze.js";
2
+ export declare function runInteractive(analysis: AnalysisResult): Promise<Fix[]>;
@@ -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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"}
@@ -0,0 +1,2 @@
1
+ import type { AnalysisResult, Fix } from "./analyze.js";
2
+ export declare function generateReport(url: string, referencePath: string, capturedPath: string, diffPath: string, diffPercent: number, analysis: AnalysisResult, fixes: Fix[], outputDir: string): string;
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
+ }