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,226 @@
1
+ #!/usr/bin/env python3
2
+ """Generate a Markdown report for one branch compared with a base branch."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import re
8
+ import subprocess
9
+ from collections import defaultdict
10
+ from datetime import datetime, timezone
11
+ from pathlib import Path
12
+
13
+
14
+ STATUS_LABELS = {
15
+ "A": "Added",
16
+ "C": "Copied",
17
+ "D": "Deleted",
18
+ "M": "Modified",
19
+ "R": "Renamed",
20
+ "T": "Type Changed",
21
+ "U": "Unmerged",
22
+ "X": "Unknown",
23
+ }
24
+
25
+ AREA_RULES = (
26
+ ("Documentation", ("docs/", "README", ".md")),
27
+ ("Configuration and build", ("package.json", "package-lock.json", "webpack", "babel", "jest", "config/", ".eslintrc", ".prettierrc", ".nvmrc")),
28
+ ("Routes and pages", ("overrides/app/routes", "overrides/app/pages/")),
29
+ ("Components", ("overrides/app/components/",)),
30
+ ("Hooks and data access", ("overrides/app/hooks/", "commerce-sdk-react", "useCustom")),
31
+ ("Checkout and cart", ("checkout", "cart", "basket", "payment", "paypal", "applepay", "amazon")),
32
+ ("Product and catalog", ("product", "plp", "pdp", "category", "algolia")),
33
+ ("Account and auth", ("account", "login", "registration", "password", "wishlist")),
34
+ ("Integrations", ("braintree", "amazon", "algolia", "stylitics", "powerreviews", "marketing", "agentforce", "spectrum", "osano")),
35
+ ("Tests", (".test.", ".spec.", "tests/")),
36
+ ("Translations and assets", ("translations/", "assets/", ".svg", ".woff", ".png", ".jpg", ".webp")),
37
+ )
38
+
39
+
40
+ def run_git(repo: Path, args: list[str], check: bool = True) -> str:
41
+ result = subprocess.run(
42
+ ["git", *args],
43
+ cwd=repo,
44
+ text=True,
45
+ stdout=subprocess.PIPE,
46
+ stderr=subprocess.PIPE,
47
+ check=False,
48
+ )
49
+ if check and result.returncode != 0:
50
+ message = result.stderr.strip() or result.stdout.strip()
51
+ raise SystemExit(f"git {' '.join(args)} failed: {message}")
52
+ return result.stdout.strip()
53
+
54
+
55
+ def resolve_ref(repo: Path, ref: str) -> str:
56
+ candidates = [ref]
57
+ if not ref.startswith("origin/"):
58
+ candidates.append(f"origin/{ref}")
59
+ for candidate in candidates:
60
+ result = subprocess.run(
61
+ ["git", "rev-parse", "--verify", f"{candidate}^{{commit}}"],
62
+ cwd=repo,
63
+ text=True,
64
+ stdout=subprocess.PIPE,
65
+ stderr=subprocess.PIPE,
66
+ check=False,
67
+ )
68
+ if result.returncode == 0:
69
+ return candidate
70
+ raise SystemExit(f"Branch or ref not found: {ref}")
71
+
72
+
73
+ def safe_name(branch: str) -> str:
74
+ cleaned = re.sub(r"[^A-Za-z0-9._-]+", "-", branch).strip("-")
75
+ return cleaned or "branch"
76
+
77
+
78
+ def parse_name_status(raw: str) -> dict[str, list[str]]:
79
+ grouped: dict[str, list[str]] = defaultdict(list)
80
+ for line in raw.splitlines():
81
+ if not line.strip():
82
+ continue
83
+ parts = line.split("\t")
84
+ status = parts[0]
85
+ label_key = status[0]
86
+ if label_key == "R" and len(parts) >= 3:
87
+ path = f"{parts[1]} -> {parts[2]}"
88
+ else:
89
+ path = parts[-1]
90
+ grouped[label_key].append(path)
91
+ return dict(sorted(grouped.items()))
92
+
93
+
94
+ def infer_areas(files: list[str], commits: str) -> list[str]:
95
+ haystack = "\n".join(files) + "\n" + commits
96
+ lower = haystack.lower()
97
+ summaries: list[str] = []
98
+ for area, needles in AREA_RULES:
99
+ matches = [needle for needle in needles if needle.lower() in lower]
100
+ if not matches:
101
+ continue
102
+ examples = [path for path in files if any(needle.lower() in path.lower() for needle in needles)]
103
+ example_text = ""
104
+ if examples:
105
+ sample = ", ".join(examples[:4])
106
+ more = f" and {len(examples) - 4} more" if len(examples) > 4 else ""
107
+ example_text = f" Touched: {sample}{more}."
108
+ summaries.append(f"- {area}: changes are indicated by matching paths or commit messages.{example_text}")
109
+ if summaries:
110
+ return summaries
111
+ return ["- General code changes: inspect the changed files and commits for implementation details."]
112
+
113
+
114
+ def render_report(
115
+ repo: Path,
116
+ branch_input: str,
117
+ branch_ref: str,
118
+ base_ref: str,
119
+ merge_base: str,
120
+ ahead: str,
121
+ behind: str,
122
+ commits: str,
123
+ grouped_files: dict[str, list[str]],
124
+ diffstat: str,
125
+ ) -> str:
126
+ generated = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
127
+ all_files = [path for paths in grouped_files.values() for path in paths]
128
+ test_files = [path for path in all_files if ".test." in path or ".spec." in path or path.startswith("tests/")]
129
+
130
+ lines: list[str] = [
131
+ f"# Branch Change Report: `{branch_input}`",
132
+ "",
133
+ "## Metadata",
134
+ "",
135
+ f"- Branch: `{branch_ref}`",
136
+ f"- Base: `{base_ref}`",
137
+ f"- Merge base: `{merge_base}`",
138
+ f"- Generated: {generated}",
139
+ f"- Ahead of base: {ahead} commit(s)",
140
+ f"- Behind base: {behind} commit(s)",
141
+ "",
142
+ "## Unique Commits",
143
+ "",
144
+ ]
145
+ if commits:
146
+ lines.extend(commits.splitlines())
147
+ else:
148
+ lines.append("- No commits unique to this branch.")
149
+
150
+ lines.extend(["", "## Changed Files", ""])
151
+ if grouped_files:
152
+ for status, files in grouped_files.items():
153
+ label = STATUS_LABELS.get(status, status)
154
+ lines.extend([f"### {label}", ""])
155
+ for path in files:
156
+ lines.append(f"- `{path}`")
157
+ lines.append("")
158
+ else:
159
+ lines.append("- No changed files found.")
160
+ lines.append("")
161
+
162
+ lines.extend(["## Diffstat", ""])
163
+ if diffstat:
164
+ lines.extend(["```text", diffstat, "```"])
165
+ else:
166
+ lines.append("- No diffstat available.")
167
+
168
+ lines.extend(["", "## Impact Summary", ""])
169
+ lines.extend(infer_areas(all_files, commits))
170
+
171
+ lines.extend(["", "## Testing Notes", ""])
172
+ if test_files:
173
+ lines.append("Test files touched in this branch:")
174
+ lines.append("")
175
+ for path in test_files:
176
+ lines.append(f"- `{path}`")
177
+ else:
178
+ lines.append("- No test files were changed in this branch diff.")
179
+ lines.append("- This report generator does not run tests; runtime verification is not included.")
180
+
181
+ lines.extend(["", "## Verification Gaps", ""])
182
+ lines.append("- Review the changed source files for business behavior before treating this report as release documentation.")
183
+ lines.append("- Full patch output is intentionally omitted by default.")
184
+ lines.append("")
185
+ return "\n".join(lines)
186
+
187
+
188
+ def main() -> int:
189
+ parser = argparse.ArgumentParser(description=__doc__)
190
+ parser.add_argument("--branch", required=True, help="Branch or ref to report on.")
191
+ parser.add_argument("--base", default="origin/main", help="Base branch or ref. Defaults to origin/main.")
192
+ parser.add_argument("--output-dir", default="docs/branch-change-reports", help="Output directory for the report.")
193
+ parser.add_argument("--repo", default=".", help="Repository root. Defaults to current directory.")
194
+ args = parser.parse_args()
195
+
196
+ repo = Path(args.repo).resolve()
197
+ branch_ref = resolve_ref(repo, args.branch)
198
+ base_ref = resolve_ref(repo, args.base)
199
+ merge_base = run_git(repo, ["merge-base", base_ref, branch_ref])
200
+ behind, ahead = run_git(repo, ["rev-list", "--left-right", "--count", f"{base_ref}...{branch_ref}"]).split()
201
+ commits = run_git(repo, ["log", "--pretty=format:- `%h` %s", f"{base_ref}..{branch_ref}"], check=False)
202
+ name_status = run_git(repo, ["diff", "--name-status", f"{base_ref}...{branch_ref}"], check=False)
203
+ diffstat = run_git(repo, ["diff", "--stat", f"{base_ref}...{branch_ref}"], check=False)
204
+
205
+ output_dir = repo / args.output_dir
206
+ output_dir.mkdir(parents=True, exist_ok=True)
207
+ output_path = output_dir / f"{safe_name(args.branch)}.md"
208
+ report = render_report(
209
+ repo,
210
+ args.branch,
211
+ branch_ref,
212
+ base_ref,
213
+ merge_base,
214
+ ahead,
215
+ behind,
216
+ commits,
217
+ parse_name_status(name_status),
218
+ diffstat,
219
+ )
220
+ output_path.write_text(report, encoding="utf-8")
221
+ print(output_path)
222
+ return 0
223
+
224
+
225
+ if __name__ == "__main__":
226
+ raise SystemExit(main())
@@ -0,0 +1,53 @@
1
+ ---
2
+ name: project-docs-sync
3
+ description: Sync whole-project documentation with the current codebase. Use when asked to audit or refresh project docs, regenerate comprehensive docs, align docs with source code, document architecture, routes, APIs, environment variables, testing, deployment, or make stale documentation source-backed and current.
4
+ ---
5
+
6
+ # Project Docs Sync
7
+
8
+ Use this skill when the task is to synchronize project documentation with the actual codebase.
9
+
10
+ ## Core Rules
11
+
12
+ - Inspect the repository before editing docs. Prefer `rg`, `find`, `git status`, package manifests, config files, routes, tests, and existing docs.
13
+ - Update existing documentation instead of duplicating it.
14
+ - Document only source-backed behavior. If behavior is inferred or externally owned, label it clearly as an assumption or external dependency.
15
+ - Preserve the repo's documentation style, file names, and cross-linking conventions.
16
+ - Keep source references concrete enough for maintainers to verify.
17
+ - Do not change runtime code unless the user explicitly asks for code changes.
18
+
19
+ ## Recommended Workflow
20
+
21
+ 1. Check scope.
22
+ - Identify whether the user wants all docs refreshed or a specific documentation area.
23
+ - Run `git status --short` and avoid overwriting unrelated user changes.
24
+
25
+ 2. Gather repo facts.
26
+ - Run `python3 scripts/audit_project_docs.py` from the repo root (or skill directory).
27
+ - Supplement the audit with targeted reads of relevant files.
28
+ - For web apps, inspect routes, SSR/server handlers, package scripts, config files, env var references, tests, and integration points.
29
+
30
+ 3. Map source to documentation.
31
+ - Architecture docs should come from app entrypoints, routes, server handlers, providers, state/data hooks, and config.
32
+ - API docs should include local server endpoints, generated or framework routes, and external/custom API calls used by the app.
33
+ - Environment docs should include env vars found in source, package scripts, deployment config, and `.env*` files when present.
34
+ - Database docs should describe actual persistence. If there is no local database, document the external data stores and domain entities instead of inventing tables.
35
+
36
+ 4. Edit docs.
37
+ - Keep sections substantial but maintainable.
38
+ - Add Mermaid diagrams when they clarify architecture, flows, lifecycle, or state transitions.
39
+ - Cross-link related docs.
40
+ - Mark unsupported, absent, or unclear behavior explicitly.
41
+
42
+ 5. Verify.
43
+ - Confirm requested docs exist and contain real content.
44
+ - Check Markdown links/headings where practical.
45
+ - Run `git diff -- docs` and ensure changes are documentation-only unless otherwise requested.
46
+ - Report verification gaps honestly.
47
+
48
+ ## Completion Criteria
49
+
50
+ - Docs reflect the current source tree and config.
51
+ - Missing or unclear behavior is labeled rather than assumed.
52
+ - New docs are discoverable from the docs index or README.
53
+ - The final response lists edited docs and verification performed.
@@ -0,0 +1,7 @@
1
+ interface:
2
+ display_name: "Project Docs Sync"
3
+ short_description: "Sync project docs with source code"
4
+ default_prompt: "Use $project-docs-sync to audit this repository and update the project documentation from source-backed behavior."
5
+
6
+ policy:
7
+ allow_implicit_invocation: true
@@ -0,0 +1,169 @@
1
+ #!/usr/bin/env python3
2
+ """Print source-backed facts useful for syncing project documentation."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import re
9
+ from pathlib import Path
10
+
11
+
12
+ ENV_RE = re.compile(r"process\.env\.([A-Z0-9_]+)")
13
+ ROUTE_RE = re.compile(r"path:\s*['\"]([^'\"]+)['\"]")
14
+ API_NAME_RE = re.compile(r"apiName:\s*['\"]([^'\"]+)['\"]")
15
+ ENDPOINT_RE = re.compile(r"endpointPath:\s*['\"]([^'\"]+)['\"]")
16
+ METHOD_RE = re.compile(r"method:\s*['\"]([^'\"]+)['\"]")
17
+
18
+
19
+ def iter_files(root: Path, patterns: tuple[str, ...]) -> list[Path]:
20
+ files: list[Path] = []
21
+ for pattern in patterns:
22
+ files.extend(root.glob(pattern))
23
+ return sorted({path for path in files if path.is_file()})
24
+
25
+
26
+ def read_text(path: Path) -> str:
27
+ try:
28
+ return path.read_text(encoding="utf-8")
29
+ except UnicodeDecodeError:
30
+ return path.read_text(encoding="utf-8", errors="ignore")
31
+
32
+
33
+ def rel(root: Path, path: Path) -> str:
34
+ try:
35
+ return str(path.relative_to(root))
36
+ except ValueError:
37
+ return str(path)
38
+
39
+
40
+ def print_section(title: str) -> None:
41
+ print(f"\n## {title}")
42
+
43
+
44
+ def package_summary(root: Path) -> None:
45
+ print_section("Package")
46
+ package_path = root / "package.json"
47
+ if not package_path.exists():
48
+ print("- package.json: not found")
49
+ return
50
+
51
+ data = json.loads(read_text(package_path))
52
+ print(f"- name: {data.get('name', 'unknown')}")
53
+ print(f"- version: {data.get('version', 'unknown')}")
54
+ print(f"- engines: {json.dumps(data.get('engines', {}), sort_keys=True)}")
55
+ scripts = data.get("scripts", {})
56
+ print("- scripts:")
57
+ for name in sorted(scripts):
58
+ print(f" - {name}: {scripts[name]}")
59
+ print(f"- dependencies: {len(data.get('dependencies', {}))}")
60
+ print(f"- devDependencies: {len(data.get('devDependencies', {}))}")
61
+
62
+
63
+ def config_summary(root: Path) -> None:
64
+ print_section("Config Files")
65
+ files = iter_files(root, ("config/*.js", "*.config.js", ".*rc", ".nvmrc", ".prettierrc*", ".eslintrc*"))
66
+ if not files:
67
+ print("- none found")
68
+ return
69
+ for path in files:
70
+ print(f"- {rel(root, path)}")
71
+
72
+
73
+ def docs_summary(root: Path) -> None:
74
+ print_section("Existing Docs")
75
+ files = iter_files(root, ("docs/**/*.md", "*.md", "*.MD"))
76
+ if not files:
77
+ print("- none found")
78
+ return
79
+ for path in files:
80
+ print(f"- {rel(root, path)}")
81
+
82
+
83
+ def env_summary(root: Path) -> None:
84
+ print_section("Environment Variables")
85
+ found: dict[str, set[str]] = {}
86
+ files = iter_files(root, ("**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx", "package.json", ".env*"))
87
+ for path in files:
88
+ if "node_modules" in path.parts or "build" in path.parts:
89
+ continue
90
+ for match in ENV_RE.finditer(read_text(path)):
91
+ found.setdefault(match.group(1), set()).add(rel(root, path))
92
+ if not found:
93
+ print("- none found")
94
+ return
95
+ for name in sorted(found):
96
+ print(f"- {name}: {', '.join(sorted(found[name]))}")
97
+
98
+
99
+ def routes_summary(root: Path) -> None:
100
+ print_section("Routes")
101
+ route_files = iter_files(root, ("**/routes.jsx", "**/routes.js", "**/routes.tsx", "**/routes.ts"))
102
+ if not route_files:
103
+ print("- no route files found")
104
+ return
105
+ for path in route_files:
106
+ if "node_modules" in path.parts:
107
+ continue
108
+ routes = ROUTE_RE.findall(read_text(path))
109
+ print(f"- {rel(root, path)}")
110
+ for route in routes:
111
+ print(f" - {route}")
112
+
113
+
114
+ def custom_api_summary(root: Path) -> None:
115
+ print_section("Custom API Usages")
116
+ files = iter_files(root, ("**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"))
117
+ rows: list[tuple[str, str, str, str]] = []
118
+ for path in files:
119
+ if "node_modules" in path.parts or "build" in path.parts or "static" in path.parts:
120
+ continue
121
+ text = read_text(path)
122
+ api_names = API_NAME_RE.findall(text)
123
+ endpoints = ENDPOINT_RE.findall(text)
124
+ methods = METHOD_RE.findall(text)
125
+ if not api_names and not endpoints:
126
+ continue
127
+ count = max(len(api_names), len(endpoints), 1)
128
+ for index in range(count):
129
+ api_name = api_names[index] if index < len(api_names) else "unknown"
130
+ endpoint = endpoints[index] if index < len(endpoints) else "unknown"
131
+ method = methods[index] if index < len(methods) else "unknown"
132
+ rows.append((api_name, endpoint, method, rel(root, path)))
133
+ if not rows:
134
+ print("- none found")
135
+ return
136
+ for api_name, endpoint, method, source in sorted(rows):
137
+ print(f"- {api_name}/{endpoint} [{method}] in {source}")
138
+
139
+
140
+ def tests_summary(root: Path) -> None:
141
+ print_section("Tests")
142
+ files = iter_files(root, ("**/*.test.js", "**/*.test.jsx", "**/*.spec.js", "**/*.spec.jsx", "tests/**/*"))
143
+ files = [path for path in files if "node_modules" not in path.parts and "build" not in path.parts]
144
+ if not files:
145
+ print("- none found")
146
+ return
147
+ for path in files:
148
+ print(f"- {rel(root, path)}")
149
+
150
+
151
+ def main() -> int:
152
+ parser = argparse.ArgumentParser(description=__doc__)
153
+ parser.add_argument("--repo", default=".", help="Repository root. Defaults to current directory.")
154
+ args = parser.parse_args()
155
+
156
+ root = Path(args.repo).resolve()
157
+ print(f"# Project Documentation Audit: {root}")
158
+ package_summary(root)
159
+ config_summary(root)
160
+ docs_summary(root)
161
+ env_summary(root)
162
+ routes_summary(root)
163
+ custom_api_summary(root)
164
+ tests_summary(root)
165
+ return 0
166
+
167
+
168
+ if __name__ == "__main__":
169
+ raise SystemExit(main())
@@ -0,0 +1,90 @@
1
+ ---
2
+ name: ui-visual-feedback-loop
3
+ description: Implement and verify frontend UIs from screenshots, mockups, design images, or Figma references using an iterative visual feedback loop. Supports Chrome DevTools MCP browser inspection as well as automated Playwright capture and visual comparison scripts. Use when building or refining a UI component/page from visual references, taking live screenshots, comparing against the reference, and iterating until high visual parity is achieved.
4
+ ---
5
+
6
+ # UI Visual Feedback Loop
7
+
8
+ Use this skill when implementing or refining a UI from a reference image, mockup, screenshot, or design URL, and iteratively verifying the live rendering against the reference.
9
+
10
+ ## Primary Workflows
11
+
12
+ Choose the workflow suited to your environment:
13
+ - **Workflow A: Chrome DevTools MCP** (Interactive live inspection via Chrome DevTools MCP tools).
14
+ - **Workflow B: Playwright Automated Scripts** (Automated script-based screenshot capture and visual mismatch diffing).
15
+
16
+ ---
17
+
18
+ ## Reference Intake
19
+
20
+ 1. **Inspect Reference**:
21
+ - If the input is a Figma link, inspect the frame/node in Chrome using the `figma` skill.
22
+ - Inspect the target layout regions, viewport width/height, colors, spacing rhythm, typography, buttons, forms, and responsive breakpoints.
23
+ 2. **Setup Local Surface**:
24
+ - Identify or start the local web application dev server.
25
+ - Ensure the page renders deterministically (use seed mock data if needed).
26
+
27
+ ---
28
+
29
+ ## Workflow A: Chrome DevTools MCP (Recommended for Interactive Agents)
30
+
31
+ 1. **Launch & Connect**:
32
+ - Start the local dev server (e.g. `http://localhost:3000`).
33
+ - Open target page with Chrome DevTools MCP tools (`open_url` / `navigate_to`).
34
+ - Set browser dimensions matching reference (`resize_page` or `emulate`).
35
+
36
+ 2. **First Pass Implementation**:
37
+ - Match structure, layout hierarchy, and design tokens (colors, fonts, padding/margins) before polishing micro-details.
38
+
39
+ 3. **Iterative Verification Loop**:
40
+ - Take live screenshot (`take_screenshot`).
41
+ - Take accessibility/DOM snapshot (`take_snapshot`) for layout structure checks.
42
+ - Use `evaluate_script` to inspect computed styles, element bounding boxes, font sizes, and scroll overflows.
43
+ - Check `list_console_messages` or `list_network_requests` if assets or data fail to render.
44
+ - Dismiss dialogs, popups, or tooltips before capturing screenshots.
45
+
46
+ 4. **Compare & Refine**:
47
+ - Compare live screenshot with reference image.
48
+ - Apply targeted code fixes (alignment, font weight, border-radius, container widths, flex/grid rules).
49
+ - Re-capture fresh Chrome DevTools screenshot after every edit cycle.
50
+
51
+ ---
52
+
53
+ ## Workflow B: Playwright Automated Scripts (Scripted Visual Diffing)
54
+
55
+ 1. **Prerequisites**:
56
+ - Ensure Playwright helper script dependencies are present:
57
+ ```bash
58
+ npm install -D playwright sharp pixelmatch pngjs
59
+ ```
60
+
61
+ 2. **Run Orchestration Loop**:
62
+ ```bash
63
+ bash scripts/ui_loop.sh \
64
+ --reference path/to/reference.png \
65
+ --url http://127.0.0.1:3000 \
66
+ --start "npm run dev -- --host 127.0.0.1 --port 3000" \
67
+ --max-loops 5
68
+ ```
69
+
70
+ 3. **Manual Capture & Diff Step**:
71
+ ```bash
72
+ node scripts/capture.js --url http://127.0.0.1:3000 --out artifacts/latest.png
73
+ node scripts/compare.js --reference path/to/reference.png --candidate artifacts/latest.png --diff artifacts/diff.png --json artifacts/report.json
74
+ ```
75
+
76
+ 4. **Interpret Visual Diff Output**:
77
+ - Review `artifacts/report.json` mismatch metrics and `artifacts/diff.png`.
78
+ - Update code to fix visual discrepancies highlighted in diff report.
79
+
80
+ ---
81
+
82
+ ## General Operating Rules
83
+
84
+ - Default to 3 to 5 iteration loops. Stop when visual gaps are minor or blocked by missing font assets.
85
+ - Do not mark tasks complete without fetching a final post-edit live screenshot.
86
+ - State clearly:
87
+ 1. What the reference shows
88
+ 2. What code changes were applied
89
+ 3. What the latest screenshot/diff confirms
90
+ 4. What remaining discrepancies exist (if any)
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "ui-visual-feedback-support",
3
+ "private": true,
4
+ "version": "1.0.0",
5
+ "description": "Support scripts for the ui-visual-feedback-loop Agent Skill",
6
+ "scripts": {
7
+ "capture": "node scripts/capture.js",
8
+ "compare": "node scripts/compare.js",
9
+ "ui:loop": "bash scripts/ui_loop.sh"
10
+ },
11
+ "devDependencies": {
12
+ "pixelmatch": "^6.0.0",
13
+ "playwright": "^1.53.0",
14
+ "pngjs": "^7.0.0",
15
+ "sharp": "^0.34.0"
16
+ }
17
+ }
@@ -0,0 +1,17 @@
1
+ # Notes for UI Visual Feedback Loop
2
+
3
+ ## Features
4
+ - Works with live Chrome DevTools MCP tools for interactive agent feedback.
5
+ - Uses Playwright for headless scripted automated captures.
6
+ - Produces image diffs (`artifacts/diff.png`) and structured metrics (`artifacts/report.json`).
7
+ - Standardizes visual verification across web projects.
8
+
9
+ ## Optional Extensions
10
+ - Add custom viewport presets (e.g. mobile 375x812, tablet 768x1024, desktop 1440x900).
11
+ - Add screenshot element-cropping for component-isolated diffing.
12
+
13
+ ## Support npm packages
14
+ ```bash
15
+ npm install -D playwright sharp pixelmatch pngjs
16
+ npx playwright install chromium
17
+ ```
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const { chromium } = require('playwright');
5
+ const sharp = require('sharp');
6
+
7
+ function arg(name, fallback = undefined) {
8
+ const idx = process.argv.indexOf(name);
9
+ return idx >= 0 ? process.argv[idx + 1] : fallback;
10
+ }
11
+
12
+ async function inferViewportFromReference(referencePath) {
13
+ if (!referencePath || !fs.existsSync(referencePath)) return null;
14
+ const meta = await sharp(referencePath).metadata();
15
+ if (!meta.width || !meta.height) return null;
16
+ return { width: meta.width, height: meta.height };
17
+ }
18
+
19
+ (async () => {
20
+ const url = arg('--url');
21
+ const out = arg('--out', 'artifacts/latest.png');
22
+ const waitMs = Number(arg('--wait-ms', '1500'));
23
+ const reference = arg('--reference');
24
+ let width = Number(arg('--viewport-width', '0'));
25
+ let height = Number(arg('--viewport-height', '0'));
26
+ const fullPage = !process.argv.includes('--no-full-page');
27
+
28
+ if (!url) {
29
+ console.error('Missing --url');
30
+ process.exit(1);
31
+ }
32
+
33
+ if (!width || !height) {
34
+ const inferred = await inferViewportFromReference(reference);
35
+ if (inferred) {
36
+ width = width || inferred.width;
37
+ height = height || inferred.height;
38
+ }
39
+ }
40
+
41
+ width = width || 1440;
42
+ height = height || 1024;
43
+
44
+ fs.mkdirSync(path.dirname(out), { recursive: true });
45
+
46
+ const browser = await chromium.launch({ headless: true });
47
+ const page = await browser.newPage({ viewport: { width, height }, deviceScaleFactor: 1 });
48
+
49
+ await page.goto(url, { waitUntil: 'networkidle', timeout: 120000 });
50
+ await page.addStyleTag({
51
+ content: `
52
+ *, *::before, *::after {
53
+ animation-duration: 0s !important;
54
+ animation-delay: 0s !important;
55
+ transition-duration: 0s !important;
56
+ caret-color: transparent !important;
57
+ }
58
+ html { scroll-behavior: auto !important; }
59
+ `,
60
+ });
61
+ await page.waitForTimeout(waitMs);
62
+ await page.screenshot({ path: out, fullPage });
63
+ await browser.close();
64
+
65
+ console.log(JSON.stringify({ ok: true, out, viewport: { width, height }, fullPage }, null, 2));
66
+ })().catch((err) => {
67
+ console.error(err);
68
+ process.exit(1);
69
+ });