howone 0.1.52 → 0.1.53
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/package.json +1 -1
- package/templates/vite/.howone/skills/web-clone/LICENSE +21 -0
- package/templates/vite/.howone/skills/web-clone/README.md +179 -0
- package/templates/vite/.howone/skills/web-clone/SKILL.md +243 -0
- package/templates/vite/.howone/skills/web-clone/references/assessment.md +77 -0
- package/templates/vite/.howone/skills/web-clone/references/complex-playbooks.md +46 -0
- package/templates/vite/.howone/skills/web-clone/references/deliverables.md +144 -0
- package/templates/vite/.howone/skills/web-clone/references/design-dna.md +125 -0
- package/templates/vite/.howone/skills/web-clone/references/effect-extraction.md +73 -0
- package/templates/vite/.howone/skills/web-clone/references/marbles-case.md +31 -0
- package/templates/vite/.howone/skills/web-clone/references/reverse-engineering.md +34 -0
- package/templates/vite/.howone/skills/web-clone/references/static-mirror.md +72 -0
- package/templates/vite/.howone/skills/web-clone/scripts/asset-harvest.mjs +101 -0
- package/templates/vite/.howone/skills/web-clone/scripts/audit-clone.mjs +151 -0
- package/templates/vite/.howone/skills/web-clone/scripts/compare-recon.mjs +265 -0
- package/templates/vite/.howone/skills/web-clone/scripts/dna-scaffold.mjs +214 -0
- package/templates/vite/.howone/skills/web-clone/scripts/init-clone.mjs +136 -0
- package/templates/vite/.howone/skills/web-clone/scripts/interaction-probe.mjs +314 -0
- package/templates/vite/.howone/skills/web-clone/scripts/lib/playwright-loader.mjs +39 -0
- package/templates/vite/.howone/skills/web-clone/scripts/mirror-site.mjs +121 -0
- package/templates/vite/.howone/skills/web-clone/scripts/network-capture.mjs +127 -0
- package/templates/vite/.howone/skills/web-clone/scripts/recon-site.mjs +235 -0
- package/templates/vite/.howone/skills/web-clone/scripts/route-crawl.mjs +228 -0
- package/templates/vite/.howone/skills/web-clone/scripts/sourcemap-hunt.mjs +112 -0
- package/templates/vite/.howone/skills/web-clone/scripts/visual-diff.mjs +161 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { loadPlaywright, launchChromium } from "./lib/playwright-loader.mjs";
|
|
5
|
+
|
|
6
|
+
function usage() {
|
|
7
|
+
console.log(`Usage:
|
|
8
|
+
node scripts/visual-diff.mjs --original <png> --clone <png> --out visual-diff.json [--diff visual-diff.png] [--threshold 0.08]
|
|
9
|
+
|
|
10
|
+
Compares screenshots in a real browser canvas and outputs numeric visual-diff metrics.
|
|
11
|
+
`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function parseArgs(argv) {
|
|
15
|
+
const out = { original: "", clone: "", out: "visual-diff.json", diff: "", threshold: 0.08 };
|
|
16
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
17
|
+
const arg = argv[i];
|
|
18
|
+
if (arg === "--help" || arg === "-h") out.help = true;
|
|
19
|
+
else if (arg === "--original") out.original = argv[++i] || "";
|
|
20
|
+
else if (arg === "--clone") out.clone = argv[++i] || "";
|
|
21
|
+
else if (arg === "--out") out.out = argv[++i] || "visual-diff.json";
|
|
22
|
+
else if (arg === "--diff") out.diff = argv[++i] || "";
|
|
23
|
+
else if (arg === "--threshold") out.threshold = Number(argv[++i] || "0.08");
|
|
24
|
+
else throw new Error(`Unexpected argument: ${arg}`);
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function imageDataUrl(file) {
|
|
30
|
+
const ext = path.extname(file).slice(1).toLowerCase() || "png";
|
|
31
|
+
const mime = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : "image/png";
|
|
32
|
+
return `data:${mime};base64,${fs.readFileSync(file).toString("base64")}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function scoreFromDiff(diffRatio, meanAbsDiff) {
|
|
36
|
+
if (diffRatio <= 0.01 && meanAbsDiff <= 0.01) return 5;
|
|
37
|
+
if (diffRatio <= 0.04 && meanAbsDiff <= 0.025) return 4.5;
|
|
38
|
+
if (diffRatio <= 0.08 && meanAbsDiff <= 0.05) return 4;
|
|
39
|
+
if (diffRatio <= 0.16 && meanAbsDiff <= 0.08) return 3;
|
|
40
|
+
if (diffRatio <= 0.3 && meanAbsDiff <= 0.14) return 2;
|
|
41
|
+
return 1;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function compareInBrowser(page, original, clone, threshold) {
|
|
45
|
+
return page.evaluate(async ({ original, clone, threshold }) => {
|
|
46
|
+
const loadImage = (src) => new Promise((resolve, reject) => {
|
|
47
|
+
const image = new Image();
|
|
48
|
+
image.onload = () => resolve(image);
|
|
49
|
+
image.onerror = () => reject(new Error(`Failed to load image: ${src.slice(0, 80)}`));
|
|
50
|
+
image.src = src;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const [left, right] = await Promise.all([loadImage(original), loadImage(clone)]);
|
|
54
|
+
const width = Math.max(left.naturalWidth, right.naturalWidth);
|
|
55
|
+
const height = Math.max(left.naturalHeight, right.naturalHeight);
|
|
56
|
+
|
|
57
|
+
const canvasA = document.createElement("canvas");
|
|
58
|
+
const canvasB = document.createElement("canvas");
|
|
59
|
+
const canvasD = document.createElement("canvas");
|
|
60
|
+
for (const canvas of [canvasA, canvasB, canvasD]) {
|
|
61
|
+
canvas.width = width;
|
|
62
|
+
canvas.height = height;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const ctxA = canvasA.getContext("2d");
|
|
66
|
+
const ctxB = canvasB.getContext("2d");
|
|
67
|
+
const ctxD = canvasD.getContext("2d");
|
|
68
|
+
ctxA.fillStyle = "white";
|
|
69
|
+
ctxB.fillStyle = "white";
|
|
70
|
+
ctxA.fillRect(0, 0, width, height);
|
|
71
|
+
ctxB.fillRect(0, 0, width, height);
|
|
72
|
+
ctxA.drawImage(left, 0, 0);
|
|
73
|
+
ctxB.drawImage(right, 0, 0);
|
|
74
|
+
|
|
75
|
+
const a = ctxA.getImageData(0, 0, width, height);
|
|
76
|
+
const b = ctxB.getImageData(0, 0, width, height);
|
|
77
|
+
const d = ctxD.createImageData(width, height);
|
|
78
|
+
let changed = 0;
|
|
79
|
+
let sumAbs = 0;
|
|
80
|
+
let sumSq = 0;
|
|
81
|
+
|
|
82
|
+
for (let i = 0; i < a.data.length; i += 4) {
|
|
83
|
+
const dr = Math.abs(a.data[i] - b.data[i]);
|
|
84
|
+
const dg = Math.abs(a.data[i + 1] - b.data[i + 1]);
|
|
85
|
+
const db = Math.abs(a.data[i + 2] - b.data[i + 2]);
|
|
86
|
+
const da = Math.abs(a.data[i + 3] - b.data[i + 3]);
|
|
87
|
+
const delta = (dr + dg + db + da) / 1020;
|
|
88
|
+
sumAbs += delta;
|
|
89
|
+
sumSq += delta * delta;
|
|
90
|
+
if (delta > threshold) changed += 1;
|
|
91
|
+
|
|
92
|
+
if (delta > threshold) {
|
|
93
|
+
d.data[i] = 255;
|
|
94
|
+
d.data[i + 1] = Math.max(0, 80 - delta * 80);
|
|
95
|
+
d.data[i + 2] = Math.max(0, 80 - delta * 80);
|
|
96
|
+
d.data[i + 3] = 255;
|
|
97
|
+
} else {
|
|
98
|
+
d.data[i] = Math.round(a.data[i] * 0.25 + 245 * 0.75);
|
|
99
|
+
d.data[i + 1] = Math.round(a.data[i + 1] * 0.25 + 245 * 0.75);
|
|
100
|
+
d.data[i + 2] = Math.round(a.data[i + 2] * 0.25 + 245 * 0.75);
|
|
101
|
+
d.data[i + 3] = 255;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
ctxD.putImageData(d, 0, 0);
|
|
106
|
+
const pixels = width * height;
|
|
107
|
+
const diffPixelRatio = changed / pixels;
|
|
108
|
+
const meanAbsDiff = sumAbs / pixels;
|
|
109
|
+
const rmse = Math.sqrt(sumSq / pixels);
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
original: { width: left.naturalWidth, height: left.naturalHeight },
|
|
113
|
+
clone: { width: right.naturalWidth, height: right.naturalHeight },
|
|
114
|
+
comparedCanvas: { width, height },
|
|
115
|
+
threshold,
|
|
116
|
+
changedPixels: changed,
|
|
117
|
+
totalPixels: pixels,
|
|
118
|
+
diffPixelRatio,
|
|
119
|
+
meanAbsDiff,
|
|
120
|
+
rmse,
|
|
121
|
+
diffPngDataUrl: canvasD.toDataURL("image/png"),
|
|
122
|
+
};
|
|
123
|
+
}, { original, clone, threshold });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const args = parseArgs(process.argv.slice(2));
|
|
128
|
+
if (args.help || !args.original || !args.clone) {
|
|
129
|
+
usage();
|
|
130
|
+
process.exit(args.help ? 0 : 1);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const { chromium } = loadPlaywright();
|
|
134
|
+
const browser = await launchChromium(chromium);
|
|
135
|
+
const page = await browser.newPage();
|
|
136
|
+
const result = await compareInBrowser(page, imageDataUrl(args.original), imageDataUrl(args.clone), args.threshold);
|
|
137
|
+
await browser.close();
|
|
138
|
+
|
|
139
|
+
const diffDataUrl = result.diffPngDataUrl;
|
|
140
|
+
delete result.diffPngDataUrl;
|
|
141
|
+
result.visualScore = scoreFromDiff(result.diffPixelRatio, result.meanAbsDiff);
|
|
142
|
+
result.files = {
|
|
143
|
+
original: path.resolve(args.original),
|
|
144
|
+
clone: path.resolve(args.clone),
|
|
145
|
+
diff: args.diff ? path.resolve(args.diff) : "",
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
fs.mkdirSync(path.dirname(path.resolve(args.out)), { recursive: true });
|
|
149
|
+
fs.writeFileSync(args.out, `${JSON.stringify(result, null, 2)}\n`);
|
|
150
|
+
|
|
151
|
+
if (args.diff) {
|
|
152
|
+
const base64 = diffDataUrl.replace(/^data:image\/png;base64,/, "");
|
|
153
|
+
fs.mkdirSync(path.dirname(path.resolve(args.diff)), { recursive: true });
|
|
154
|
+
fs.writeFileSync(args.diff, Buffer.from(base64, "base64"));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
console.log(path.resolve(args.out));
|
|
158
|
+
} catch (error) {
|
|
159
|
+
console.error(`visual-diff failed: ${error.message}`);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|