developer-agent-skills 1.0.0

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.
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env node
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const sharp = require('sharp');
5
+ const pixelmatch = require('pixelmatch');
6
+ const { PNG } = require('pngjs');
7
+
8
+ function arg(name, fallback = undefined) {
9
+ const idx = process.argv.indexOf(name);
10
+ return idx >= 0 ? process.argv[idx + 1] : fallback;
11
+ }
12
+
13
+ async function normalizeToSameCanvas(referencePath, candidatePath) {
14
+ const [rMeta, cMeta] = await Promise.all([
15
+ sharp(referencePath).metadata(),
16
+ sharp(candidatePath).metadata(),
17
+ ]);
18
+
19
+ const width = Math.max(rMeta.width || 0, cMeta.width || 0);
20
+ const height = Math.max(rMeta.height || 0, cMeta.height || 0);
21
+
22
+ async function loadRGBA(file) {
23
+ return sharp(file)
24
+ .resize({ width, height, fit: 'contain', background: { r: 255, g: 255, b: 255, alpha: 1 } })
25
+ .png()
26
+ .ensureAlpha()
27
+ .raw()
28
+ .toBuffer({ resolveWithObject: true });
29
+ }
30
+
31
+ const [ref, cand] = await Promise.all([loadRGBA(referencePath), loadRGBA(candidatePath)]);
32
+ return { width, height, ref: ref.data, cand: cand.data };
33
+ }
34
+
35
+ function channelAverage(buf, offset) {
36
+ let sum = 0;
37
+ for (let i = offset; i < buf.length; i += 4) sum += buf[i];
38
+ return sum / (buf.length / 4);
39
+ }
40
+
41
+ function edgeWeightedMismatch(ref, cand, width, height) {
42
+ let sum = 0;
43
+ let count = 0;
44
+ for (let y = 1; y < height - 1; y++) {
45
+ for (let x = 1; x < width - 1; x++) {
46
+ const i = (y * width + x) * 4;
47
+ const left = i - 4;
48
+ const right = i + 4;
49
+ const up = i - width * 4;
50
+ const down = i + width * 4;
51
+ const edgeRef =
52
+ Math.abs(ref[left] - ref[right]) +
53
+ Math.abs(ref[up] - ref[down]) +
54
+ Math.abs(ref[left + 1] - ref[right + 1]) +
55
+ Math.abs(ref[up + 1] - ref[down + 1]) +
56
+ Math.abs(ref[left + 2] - ref[right + 2]) +
57
+ Math.abs(ref[up + 2] - ref[down + 2]);
58
+ const pixDiff =
59
+ Math.abs(ref[i] - cand[i]) +
60
+ Math.abs(ref[i + 1] - cand[i + 1]) +
61
+ Math.abs(ref[i + 2] - cand[i + 2]);
62
+ if (edgeRef > 60) {
63
+ sum += pixDiff;
64
+ count++;
65
+ }
66
+ }
67
+ }
68
+ return count ? sum / count : 0;
69
+ }
70
+
71
+ (async () => {
72
+ const reference = arg('--reference');
73
+ const candidate = arg('--candidate');
74
+ const diffOut = arg('--diff', 'artifacts/diff.png');
75
+ const jsonOut = arg('--json', 'artifacts/report.json');
76
+
77
+ if (!reference || !candidate) {
78
+ console.error('Missing --reference or --candidate');
79
+ process.exit(1);
80
+ }
81
+
82
+ const { width, height, ref, cand } = await normalizeToSameCanvas(reference, candidate);
83
+
84
+ const refPng = new PNG({ width, height });
85
+ const candPng = new PNG({ width, height });
86
+ ref.copy(refPng.data);
87
+ cand.copy(candPng.data);
88
+
89
+ const diff = new PNG({ width, height });
90
+ const mismatchPixels = pixelmatch(refPng.data, candPng.data, diff.data, width, height, {
91
+ threshold: 0.12,
92
+ includeAA: true,
93
+ alpha: 0.6,
94
+ });
95
+
96
+ fs.mkdirSync(path.dirname(diffOut), { recursive: true });
97
+ fs.mkdirSync(path.dirname(jsonOut), { recursive: true });
98
+ fs.writeFileSync(diffOut, PNG.sync.write(diff));
99
+
100
+ const totalPixels = width * height;
101
+ const mismatchPercent = Number(((mismatchPixels / totalPixels) * 100).toFixed(2));
102
+
103
+ const refBrightness = (channelAverage(ref, 0) + channelAverage(ref, 1) + channelAverage(ref, 2)) / 3;
104
+ const candBrightness = (channelAverage(cand, 0) + channelAverage(cand, 1) + channelAverage(cand, 2)) / 3;
105
+ const brightnessDelta = Number((candBrightness - refBrightness).toFixed(2));
106
+
107
+ const edgeDelta = Number(edgeWeightedMismatch(ref, cand, width, height).toFixed(2));
108
+
109
+ const findings = [];
110
+ if (mismatchPercent > 10) findings.push({ severity: 'high', message: 'Large structural mismatch. Major layout regions likely differ or important elements are missing.' });
111
+ else if (mismatchPercent > 4) findings.push({ severity: 'medium', message: 'Noticeable visual mismatch. Layout, spacing, or component sizing still needs refinement.' });
112
+ else if (mismatchPercent > 1.5) findings.push({ severity: 'low', message: 'Only minor differences remain. Focus on spacing, typography, and small visual polish.' });
113
+ else findings.push({ severity: 'info', message: 'Visual mismatch is small.' });
114
+
115
+ if (brightnessDelta > 8) findings.push({ severity: 'medium', message: 'Candidate appears brighter than the reference. Inspect background, surfaces, and text contrast.' });
116
+ if (brightnessDelta < -8) findings.push({ severity: 'medium', message: 'Candidate appears darker than the reference. Inspect overlays, text color, and surface colors.' });
117
+ if (edgeDelta > 80) findings.push({ severity: 'medium', message: 'Strong edge mismatch suggests alignment, spacing, or typography differences around boundaries.' });
118
+
119
+ const report = {
120
+ summary: {
121
+ width,
122
+ height,
123
+ mismatchPixels,
124
+ totalPixels,
125
+ mismatchPercent,
126
+ brightnessDelta,
127
+ edgeDelta,
128
+ },
129
+ findings,
130
+ nextActions: [
131
+ 'Check container widths, grid columns, and horizontal alignment first.',
132
+ 'Then inspect vertical spacing, font sizes, and line-height.',
133
+ 'Finally tune colors, border radius, shadows, and fine detail.'
134
+ ]
135
+ };
136
+
137
+ fs.writeFileSync(jsonOut, JSON.stringify(report, null, 2));
138
+ console.log(JSON.stringify(report, null, 2));
139
+ })().catch((err) => {
140
+ console.error(err);
141
+ process.exit(1);
142
+ });
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ REFERENCE=""
5
+ URL="http://127.0.0.1:3000"
6
+ START_CMD=""
7
+ MAX_LOOPS=5
8
+ ARTIFACTS_DIR="artifacts"
9
+ WAIT_MS=1500
10
+ VIEWPORT_WIDTH=""
11
+ VIEWPORT_HEIGHT=""
12
+
13
+ while [[ $# -gt 0 ]]; do
14
+ case "$1" in
15
+ --reference)
16
+ REFERENCE="$2"; shift 2 ;;
17
+ --url)
18
+ URL="$2"; shift 2 ;;
19
+ --start)
20
+ START_CMD="$2"; shift 2 ;;
21
+ --max-loops)
22
+ MAX_LOOPS="$2"; shift 2 ;;
23
+ --artifacts-dir)
24
+ ARTIFACTS_DIR="$2"; shift 2 ;;
25
+ --wait-ms)
26
+ WAIT_MS="$2"; shift 2 ;;
27
+ --viewport-width)
28
+ VIEWPORT_WIDTH="$2"; shift 2 ;;
29
+ --viewport-height)
30
+ VIEWPORT_HEIGHT="$2"; shift 2 ;;
31
+ *)
32
+ echo "Unknown argument: $1" >&2
33
+ exit 1 ;;
34
+ esac
35
+ done
36
+
37
+ if [[ -z "$REFERENCE" ]]; then
38
+ echo "Missing --reference path/to/reference.png" >&2
39
+ exit 1
40
+ fi
41
+
42
+ mkdir -p "$ARTIFACTS_DIR"
43
+ LOG_FILE="$ARTIFACTS_DIR/iteration-log.md"
44
+ : > "$LOG_FILE"
45
+
46
+ cleanup() {
47
+ if [[ -n "${SERVER_PID:-}" ]]; then
48
+ kill "$SERVER_PID" >/dev/null 2>&1 || true
49
+ fi
50
+ }
51
+ trap cleanup EXIT
52
+
53
+ if [[ -n "$START_CMD" ]]; then
54
+ bash -lc "$START_CMD" > "$ARTIFACTS_DIR/devserver.log" 2>&1 &
55
+ SERVER_PID=$!
56
+ echo "Started app server with PID $SERVER_PID"
57
+ sleep 3
58
+ fi
59
+
60
+ BEST_MISMATCH=""
61
+ for ((i=1; i<=MAX_LOOPS; i++)); do
62
+ echo "# Iteration $i" >> "$LOG_FILE"
63
+ CAPTURE_ARGS=(node scripts/capture.js --url "$URL" --out "$ARTIFACTS_DIR/latest.png" --wait-ms "$WAIT_MS")
64
+ if [[ -n "$VIEWPORT_WIDTH" ]]; then
65
+ CAPTURE_ARGS+=(--viewport-width "$VIEWPORT_WIDTH")
66
+ fi
67
+ if [[ -n "$VIEWPORT_HEIGHT" ]]; then
68
+ CAPTURE_ARGS+=(--viewport-height "$VIEWPORT_HEIGHT")
69
+ fi
70
+ "${CAPTURE_ARGS[@]}"
71
+
72
+ node scripts/compare.js \
73
+ --reference "$REFERENCE" \
74
+ --candidate "$ARTIFACTS_DIR/latest.png" \
75
+ --diff "$ARTIFACTS_DIR/diff.png" \
76
+ --json "$ARTIFACTS_DIR/report.json"
77
+
78
+ MISMATCH=$(node -e 'const fs=require("fs"); const p=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); process.stdout.write(String(p.summary.mismatchPercent));' "$ARTIFACTS_DIR/report.json")
79
+
80
+ echo "- mismatchPercent: $MISMATCH" >> "$LOG_FILE"
81
+ echo "- report: $ARTIFACTS_DIR/report.json" >> "$LOG_FILE"
82
+ echo "- diff: $ARTIFACTS_DIR/diff.png" >> "$LOG_FILE"
83
+ echo >> "$LOG_FILE"
84
+
85
+ if [[ -z "$BEST_MISMATCH" ]]; then
86
+ BEST_MISMATCH="$MISMATCH"
87
+ fi
88
+
89
+ if node -e 'process.exit(Number(process.argv[1]) <= 1.5 ? 0 : 1)' "$MISMATCH"; then
90
+ echo "Visual mismatch is within threshold ($MISMATCH%). Stopping."
91
+ break
92
+ fi
93
+
94
+ echo "Iteration $i complete. Review $ARTIFACTS_DIR/report.json, apply targeted UI fixes, then rerun or let Codex continue."
95
+ done
96
+
97
+ echo "Done. Latest artifacts are in $ARTIFACTS_DIR/"