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.
- package/LICENSE +21 -0
- package/README.md +127 -0
- package/bin/cli.js +104 -0
- package/index.js +119 -0
- package/package.json +48 -0
- package/skills/code-review/SKILL.md +107 -0
- package/skills/code-review/agents/openai.yaml +4 -0
- package/skills/code-review/references/review-rubric.md +38 -0
- package/skills/code-review/scripts/collect_review_context.py +414 -0
- package/skills/figma/LICENSE.txt +2 -0
- package/skills/figma/SKILL.md +50 -0
- package/skills/figma/agents/openai.yaml +14 -0
- package/skills/figma/assets/figma-small.svg +3 -0
- package/skills/figma/assets/figma.png +0 -0
- package/skills/figma/assets/icon.svg +28 -0
- package/skills/figma/references/figma-browser-inspection.md +24 -0
- package/skills/git-branch-report/SKILL.md +46 -0
- package/skills/git-branch-report/agents/openai.yaml +7 -0
- package/skills/git-branch-report/scripts/generate_branch_report.py +226 -0
- package/skills/project-docs-sync/SKILL.md +53 -0
- package/skills/project-docs-sync/agents/openai.yaml +7 -0
- package/skills/project-docs-sync/scripts/audit_project_docs.py +169 -0
- package/skills/ui-visual-feedback-loop/SKILL.md +90 -0
- package/skills/ui-visual-feedback-loop/package.json +17 -0
- package/skills/ui-visual-feedback-loop/references/notes.md +17 -0
- package/skills/ui-visual-feedback-loop/scripts/capture.js +69 -0
- package/skills/ui-visual-feedback-loop/scripts/compare.js +142 -0
- package/skills/ui-visual-feedback-loop/scripts/ui_loop.sh +97 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Collect deterministic Git context for the code-review Codex skill."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import re
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Iterable, List, Optional, Sequence, Tuple
|
|
11
|
+
|
|
12
|
+
TICKET_RE = re.compile(r"[A-Za-z][A-Za-z0-9]+-\d+")
|
|
13
|
+
VERSION_ONLY_RE = re.compile(r"v\d+", re.IGNORECASE)
|
|
14
|
+
VERSION_SUFFIX_RE = re.compile(r"(?:[-_.]+v\d+)$", re.IGNORECASE)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def run_git(repo: str, args: Sequence[str], *, check: bool = True) -> subprocess.CompletedProcess[str]:
|
|
18
|
+
proc = subprocess.run(
|
|
19
|
+
["git", "-C", repo, *args],
|
|
20
|
+
text=True,
|
|
21
|
+
stdout=subprocess.PIPE,
|
|
22
|
+
stderr=subprocess.PIPE,
|
|
23
|
+
)
|
|
24
|
+
if check and proc.returncode != 0:
|
|
25
|
+
cmd = " ".join(["git", "-C", repo, *args])
|
|
26
|
+
raise RuntimeError(f"Command failed: {cmd}\n{proc.stderr.strip()}")
|
|
27
|
+
return proc
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def git_out(repo: str, args: Sequence[str], *, check: bool = True) -> str:
|
|
31
|
+
return run_git(repo, args, check=check).stdout.strip()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def verify_repo(repo: str) -> str:
|
|
35
|
+
proc = run_git(repo, ["rev-parse", "--show-toplevel"], check=False)
|
|
36
|
+
if proc.returncode != 0:
|
|
37
|
+
raise RuntimeError(f"Not a Git repository: {repo}")
|
|
38
|
+
return proc.stdout.strip()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def worktree_dirty(repo: str) -> bool:
|
|
42
|
+
return bool(git_out(repo, ["status", "--porcelain"], check=False))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def try_git_pull(repo: str) -> List[str]:
|
|
46
|
+
warnings: List[str] = []
|
|
47
|
+
if worktree_dirty(repo):
|
|
48
|
+
warnings.append("Skipped git pull because the working tree has uncommitted changes.")
|
|
49
|
+
return warnings
|
|
50
|
+
|
|
51
|
+
proc = run_git(repo, ["pull", "--ff-only"], check=False)
|
|
52
|
+
if proc.returncode != 0:
|
|
53
|
+
detail = (proc.stderr or proc.stdout).strip().splitlines()
|
|
54
|
+
message = detail[-1] if detail else "unknown error"
|
|
55
|
+
warnings.append(f"git pull --ff-only failed; continuing with local refs. Detail: {message}")
|
|
56
|
+
return warnings
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def current_branch(repo: str) -> str:
|
|
60
|
+
branch = git_out(repo, ["branch", "--show-current"], check=False)
|
|
61
|
+
if branch:
|
|
62
|
+
return branch
|
|
63
|
+
return git_out(repo, ["rev-parse", "--abbrev-ref", "HEAD"])
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def strip_version_suffix(value: str) -> str:
|
|
67
|
+
cleaned = value
|
|
68
|
+
while True:
|
|
69
|
+
next_value = VERSION_SUFFIX_RE.sub("", cleaned)
|
|
70
|
+
if next_value == cleaned:
|
|
71
|
+
return cleaned
|
|
72
|
+
cleaned = next_value
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def derive_ticket_prefix(branch: str) -> str:
|
|
76
|
+
segments = [segment for segment in re.split(r"/+", branch.strip()) if segment]
|
|
77
|
+
usable = [segment for segment in segments if not VERSION_ONLY_RE.fullmatch(segment)] or segments or [branch]
|
|
78
|
+
|
|
79
|
+
for segment in reversed(usable):
|
|
80
|
+
cleaned = strip_version_suffix(segment)
|
|
81
|
+
match = TICKET_RE.search(cleaned)
|
|
82
|
+
if match:
|
|
83
|
+
return match.group(0)
|
|
84
|
+
|
|
85
|
+
return strip_version_suffix(usable[-1]).strip()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def ref_exists(repo: str, ref: str) -> bool:
|
|
89
|
+
proc = run_git(repo, ["rev-parse", "--verify", "--quiet", f"{ref}^{{commit}}"], check=False)
|
|
90
|
+
return proc.returncode == 0
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def remote_names(repo: str) -> List[str]:
|
|
94
|
+
out = git_out(repo, ["remote"], check=False)
|
|
95
|
+
return [line.strip() for line in out.splitlines() if line.strip()]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def strip_ref_namespace(repo: str, ref: str) -> str:
|
|
99
|
+
clean = ref.strip()
|
|
100
|
+
for prefix in ("refs/remotes/", "refs/heads/"):
|
|
101
|
+
if clean.startswith(prefix):
|
|
102
|
+
clean = clean[len(prefix) :]
|
|
103
|
+
for remote in sorted(remote_names(repo), key=len, reverse=True):
|
|
104
|
+
if clean.startswith(remote + "/"):
|
|
105
|
+
return clean[len(remote) + 1 :]
|
|
106
|
+
return clean
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def same_branch_ref(repo: str, ref: str, branch: str) -> bool:
|
|
110
|
+
return strip_ref_namespace(repo, ref) == strip_ref_namespace(repo, branch)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def detect_base_ref(repo: str, requested_base: Optional[str], branch: str) -> Tuple[Optional[str], List[str]]:
|
|
114
|
+
warnings: List[str] = []
|
|
115
|
+
if requested_base:
|
|
116
|
+
if not ref_exists(repo, requested_base):
|
|
117
|
+
raise RuntimeError(f"Base ref does not exist or is not a commit: {requested_base}")
|
|
118
|
+
return requested_base, warnings
|
|
119
|
+
|
|
120
|
+
upstream = git_out(repo, ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"], check=False)
|
|
121
|
+
if upstream and not same_branch_ref(repo, upstream, branch):
|
|
122
|
+
return upstream, warnings
|
|
123
|
+
if upstream:
|
|
124
|
+
warnings.append(f"Upstream {upstream} is the same branch; using a target base branch instead.")
|
|
125
|
+
|
|
126
|
+
for candidate in ("origin/main", "origin/master", "origin/develop", "main", "master", "develop"):
|
|
127
|
+
if ref_exists(repo, candidate):
|
|
128
|
+
if upstream:
|
|
129
|
+
warnings.append(f"Using {candidate} as base.")
|
|
130
|
+
else:
|
|
131
|
+
warnings.append(f"No upstream configured; using {candidate} as base.")
|
|
132
|
+
return candidate, warnings
|
|
133
|
+
|
|
134
|
+
warnings.append("No upstream/base branch found; commit selection may be incomplete. Pass --base or --commit.")
|
|
135
|
+
return None, warnings
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def merge_base(repo: str, base_ref: str) -> str:
|
|
139
|
+
return git_out(repo, ["merge-base", "HEAD", base_ref])
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def rev_list(repo: str, *spec_args: str) -> List[str]:
|
|
143
|
+
out = git_out(repo, ["rev-list", "--reverse", *spec_args], check=False)
|
|
144
|
+
return [line.strip() for line in out.splitlines() if line.strip()]
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def expand_commit_spec(repo: str, spec: str) -> List[str]:
|
|
148
|
+
if ".." in spec:
|
|
149
|
+
commits = rev_list(repo, spec)
|
|
150
|
+
if not commits:
|
|
151
|
+
raise RuntimeError(f"Range produced no commits: {spec}")
|
|
152
|
+
return commits
|
|
153
|
+
|
|
154
|
+
sha = git_out(repo, ["rev-parse", "--verify", f"{spec}^{{commit}}"], check=False)
|
|
155
|
+
if not sha:
|
|
156
|
+
raise RuntimeError(f"Commit/ref does not exist: {spec}")
|
|
157
|
+
return [sha]
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def dedupe_preserve_order(values: Iterable[str]) -> List[str]:
|
|
161
|
+
seen = set()
|
|
162
|
+
result = []
|
|
163
|
+
for value in values:
|
|
164
|
+
if value not in seen:
|
|
165
|
+
seen.add(value)
|
|
166
|
+
result.append(value)
|
|
167
|
+
return result
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def parse_selection(selection: Optional[str], total: int) -> List[int]:
|
|
171
|
+
if total == 0:
|
|
172
|
+
return []
|
|
173
|
+
if not selection or selection.strip().lower() == "all":
|
|
174
|
+
return list(range(total))
|
|
175
|
+
|
|
176
|
+
selected: List[int] = []
|
|
177
|
+
for raw_part in selection.split(","):
|
|
178
|
+
part = raw_part.strip()
|
|
179
|
+
if not part:
|
|
180
|
+
continue
|
|
181
|
+
if part.lower() == "all":
|
|
182
|
+
return list(range(total))
|
|
183
|
+
if "-" in part:
|
|
184
|
+
start_text, end_text = part.split("-", 1)
|
|
185
|
+
if not start_text.strip().isdigit() or not end_text.strip().isdigit():
|
|
186
|
+
raise RuntimeError(f"Invalid selection range: {part}")
|
|
187
|
+
start = int(start_text)
|
|
188
|
+
end = int(end_text)
|
|
189
|
+
if start > end:
|
|
190
|
+
start, end = end, start
|
|
191
|
+
selected.extend(range(start - 1, end))
|
|
192
|
+
else:
|
|
193
|
+
if not part.isdigit():
|
|
194
|
+
raise RuntimeError(f"Invalid selection item: {part}")
|
|
195
|
+
selected.append(int(part) - 1)
|
|
196
|
+
|
|
197
|
+
selected = dedupe_preserve_order(selected)
|
|
198
|
+
invalid = [index + 1 for index in selected if index < 0 or index >= total]
|
|
199
|
+
if invalid:
|
|
200
|
+
raise RuntimeError(f"Selection index out of range for {total} commits: {invalid}")
|
|
201
|
+
return selected
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def commit_meta(repo: str, sha: str) -> dict:
|
|
205
|
+
fmt = "%H%x00%h%x00%an%x00%ad%x00%s"
|
|
206
|
+
raw = git_out(repo, ["log", "-1", f"--format={fmt}", "--date=iso-strict", sha])
|
|
207
|
+
full, short, author, date, subject = raw.split("\x00", 4)
|
|
208
|
+
return {
|
|
209
|
+
"sha": full,
|
|
210
|
+
"short": short,
|
|
211
|
+
"author": author,
|
|
212
|
+
"date": date,
|
|
213
|
+
"subject": subject,
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def changed_files_for_commit(repo: str, sha: str) -> List[str]:
|
|
218
|
+
out = git_out(repo, ["diff-tree", "--no-commit-id", "--name-status", "-r", sha], check=False)
|
|
219
|
+
return [line for line in out.splitlines() if line.strip()]
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def collect_changed_files(repo: str, commits: Sequence[str]) -> List[str]:
|
|
223
|
+
files: List[str] = []
|
|
224
|
+
for sha in commits:
|
|
225
|
+
files.extend(changed_files_for_commit(repo, sha))
|
|
226
|
+
return dedupe_preserve_order(files)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def git_show(repo: str, sha: str) -> str:
|
|
230
|
+
return git_out(
|
|
231
|
+
repo,
|
|
232
|
+
[
|
|
233
|
+
"show",
|
|
234
|
+
"--format=fuller",
|
|
235
|
+
"--stat",
|
|
236
|
+
"--patch",
|
|
237
|
+
"--find-renames",
|
|
238
|
+
"--find-copies",
|
|
239
|
+
"--no-ext-diff",
|
|
240
|
+
"--no-color",
|
|
241
|
+
sha,
|
|
242
|
+
],
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def markdown_table(rows: Sequence[dict]) -> str:
|
|
247
|
+
lines = ["| # | SHA | Date | Author | Subject |", "|---:|---|---|---|---|"]
|
|
248
|
+
for idx, meta in enumerate(rows, 1):
|
|
249
|
+
subject = meta["subject"].replace("|", "\\|")
|
|
250
|
+
author = meta["author"].replace("|", "\\|")
|
|
251
|
+
lines.append(f"| {idx} | `{meta['short']}` | {meta['date']} | {author} | {subject} |")
|
|
252
|
+
return "\n".join(lines)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def build_context(repo: str, args: argparse.Namespace) -> Tuple[str, int]:
|
|
256
|
+
repo_root = verify_repo(repo)
|
|
257
|
+
warnings: List[str] = []
|
|
258
|
+
if not args.no_pull:
|
|
259
|
+
warnings.extend(try_git_pull(repo_root))
|
|
260
|
+
branch = current_branch(repo_root)
|
|
261
|
+
mode = "branch ticket-prefix filter"
|
|
262
|
+
base_ref = None
|
|
263
|
+
base_sha = None
|
|
264
|
+
ticket_prefix = None
|
|
265
|
+
range_spec = None
|
|
266
|
+
|
|
267
|
+
explicit_specs = []
|
|
268
|
+
explicit_specs.extend(args.commit or [])
|
|
269
|
+
explicit_specs.extend(args.range or [])
|
|
270
|
+
|
|
271
|
+
if explicit_specs:
|
|
272
|
+
mode = "explicit commit/range"
|
|
273
|
+
commits = dedupe_preserve_order(
|
|
274
|
+
sha for spec in explicit_specs for sha in expand_commit_spec(repo_root, spec)
|
|
275
|
+
)
|
|
276
|
+
else:
|
|
277
|
+
ticket_prefix = derive_ticket_prefix(branch)
|
|
278
|
+
if not ticket_prefix:
|
|
279
|
+
raise RuntimeError(f"Could not derive a ticket prefix from branch: {branch}")
|
|
280
|
+
base_ref, base_warnings = detect_base_ref(repo_root, args.base, branch)
|
|
281
|
+
warnings.extend(base_warnings)
|
|
282
|
+
if base_ref:
|
|
283
|
+
base_sha = merge_base(repo_root, base_ref)
|
|
284
|
+
range_spec = f"{base_sha}..HEAD"
|
|
285
|
+
branch_commits = rev_list(repo_root, range_spec)
|
|
286
|
+
else:
|
|
287
|
+
branch_commits = rev_list(repo_root, f"--max-count={args.max_commits}", "HEAD")
|
|
288
|
+
branch_commits = list(reversed(branch_commits))
|
|
289
|
+
|
|
290
|
+
prefix_lower = ticket_prefix.lower()
|
|
291
|
+
commits = []
|
|
292
|
+
for sha in branch_commits:
|
|
293
|
+
meta = commit_meta(repo_root, sha)
|
|
294
|
+
if meta["subject"].lower().startswith(prefix_lower):
|
|
295
|
+
commits.append(sha)
|
|
296
|
+
|
|
297
|
+
if not commits:
|
|
298
|
+
warnings.append(
|
|
299
|
+
f"No commits in the branch range have a subject starting with derived ticket prefix '{ticket_prefix}'."
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
if len(commits) > args.max_commits:
|
|
303
|
+
warnings.append(f"Truncated commit list from {len(commits)} to --max-commits={args.max_commits}.")
|
|
304
|
+
commits = commits[-args.max_commits :]
|
|
305
|
+
|
|
306
|
+
filtered_commit_count = len(commits)
|
|
307
|
+
selected_indexes = parse_selection(args.select, filtered_commit_count)
|
|
308
|
+
commits = [commits[index] for index in selected_indexes]
|
|
309
|
+
|
|
310
|
+
metadata = [commit_meta(repo_root, sha) for sha in commits]
|
|
311
|
+
changed_files = collect_changed_files(repo_root, commits)
|
|
312
|
+
|
|
313
|
+
lines: List[str] = []
|
|
314
|
+
lines.append("# Code Review Context")
|
|
315
|
+
lines.append("")
|
|
316
|
+
lines.append("## Target")
|
|
317
|
+
lines.append(f"- Repository: `{repo_root}`")
|
|
318
|
+
lines.append(f"- Mode: {mode}")
|
|
319
|
+
lines.append(f"- Branch: `{branch}`")
|
|
320
|
+
lines.append(f"- Base ref: `{base_ref or 'n/a'}`")
|
|
321
|
+
lines.append(f"- Base SHA: `{base_sha or 'n/a'}`")
|
|
322
|
+
lines.append(f"- Range: `{range_spec or ', '.join(explicit_specs) or 'n/a'}`")
|
|
323
|
+
lines.append(f"- Ticket prefix: `{ticket_prefix or 'n/a'}`")
|
|
324
|
+
lines.append(f"- Selection: `{args.select or 'all'}`")
|
|
325
|
+
lines.append(f"- Filtered commit count: {filtered_commit_count}")
|
|
326
|
+
lines.append(f"- Commit count: {len(commits)}")
|
|
327
|
+
if warnings:
|
|
328
|
+
lines.append("")
|
|
329
|
+
lines.append("## Warnings")
|
|
330
|
+
for warning in warnings:
|
|
331
|
+
lines.append(f"- {warning}")
|
|
332
|
+
lines.append("")
|
|
333
|
+
lines.append("## Commits")
|
|
334
|
+
if metadata:
|
|
335
|
+
lines.append(markdown_table(metadata))
|
|
336
|
+
else:
|
|
337
|
+
lines.append("No commits selected.")
|
|
338
|
+
lines.append("")
|
|
339
|
+
lines.append("## Changed Files")
|
|
340
|
+
if changed_files:
|
|
341
|
+
for item in changed_files:
|
|
342
|
+
lines.append(f"- `{item}`")
|
|
343
|
+
else:
|
|
344
|
+
lines.append("No changed files selected.")
|
|
345
|
+
|
|
346
|
+
if args.no_diff or not commits:
|
|
347
|
+
lines.append("")
|
|
348
|
+
lines.append("## Diffs")
|
|
349
|
+
lines.append("Diff output omitted." if args.no_diff else "No diffs selected.")
|
|
350
|
+
return "\n".join(lines) + "\n", len(commits)
|
|
351
|
+
|
|
352
|
+
lines.append("")
|
|
353
|
+
lines.append("## Diffs")
|
|
354
|
+
remaining = args.max_diff_chars
|
|
355
|
+
truncated = False
|
|
356
|
+
for meta in metadata:
|
|
357
|
+
if remaining <= 0:
|
|
358
|
+
truncated = True
|
|
359
|
+
break
|
|
360
|
+
diff = git_show(repo_root, meta["sha"])
|
|
361
|
+
if len(diff) > remaining:
|
|
362
|
+
diff = diff[:remaining]
|
|
363
|
+
truncated = True
|
|
364
|
+
remaining -= len(diff)
|
|
365
|
+
lines.append("")
|
|
366
|
+
lines.append(f"### `{meta['short']}` {meta['subject']}")
|
|
367
|
+
lines.append("```diff")
|
|
368
|
+
lines.append(diff.rstrip())
|
|
369
|
+
lines.append("```")
|
|
370
|
+
if truncated:
|
|
371
|
+
break
|
|
372
|
+
|
|
373
|
+
if truncated:
|
|
374
|
+
lines.append("")
|
|
375
|
+
lines.append(f"> Diff output truncated at --max-diff-chars={args.max_diff_chars}. Inspect remaining commits/files directly.")
|
|
376
|
+
|
|
377
|
+
return "\n".join(lines) + "\n", len(commits)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def parse_args(argv: Sequence[str]) -> argparse.Namespace:
|
|
381
|
+
parser = argparse.ArgumentParser(description="Collect Git context for a priority-sorted code review report.")
|
|
382
|
+
parser.add_argument("--repo", default=".", help="Path to the Git repository. Default: current directory.")
|
|
383
|
+
parser.add_argument("--commit", action="append", help="Commit SHA/ref or range to review. May be passed multiple times.")
|
|
384
|
+
parser.add_argument("--range", action="append", help="Git revision range to review, e.g. origin/main..HEAD. May be passed multiple times.")
|
|
385
|
+
parser.add_argument("--base", help="Base branch/ref for branch-mode selection. Default: upstream unless it is the same branch, then origin/main/master/develop.")
|
|
386
|
+
parser.add_argument("--output", help="Write Markdown context to this path instead of stdout.")
|
|
387
|
+
parser.add_argument("--max-commits", type=int, default=80, help="Maximum selected commits to include. Default: 80.")
|
|
388
|
+
parser.add_argument("--max-diff-chars", type=int, default=180000, help="Maximum combined diff characters. Default: 180000.")
|
|
389
|
+
parser.add_argument("--no-diff", action="store_true", help="Only output metadata and changed file list.")
|
|
390
|
+
parser.add_argument("--select", help="Select filtered commits by 1-based index list/range, e.g. all, 1, 2-4, 1,3-5. Default: all.")
|
|
391
|
+
parser.add_argument("--no-pull", action="store_true", help="Skip the default preflight git pull --ff-only attempt.")
|
|
392
|
+
return parser.parse_args(argv)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def main(argv: Sequence[str]) -> int:
|
|
396
|
+
args = parse_args(argv)
|
|
397
|
+
try:
|
|
398
|
+
context, count = build_context(args.repo, args)
|
|
399
|
+
except Exception as exc:
|
|
400
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
401
|
+
return 2
|
|
402
|
+
|
|
403
|
+
if args.output:
|
|
404
|
+
output_path = Path(args.output)
|
|
405
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
406
|
+
output_path.write_text(context)
|
|
407
|
+
print(f"Wrote {output_path} ({count} selected commits)")
|
|
408
|
+
else:
|
|
409
|
+
print(context, end="")
|
|
410
|
+
return 0
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
if __name__ == "__main__":
|
|
414
|
+
raise SystemExit(main(sys.argv[1:]))
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
Use of these Figma skills and related files ("Materials") is governed by the Figma Developer Terms (available at https://www.figma.com/legal/developer-terms/). By accessing, downloading, or using these Materials — including through automated systems or AI agents — you agree to the Figma Developer Terms.
|
|
2
|
+
These Materials are currently offered as a Beta feature. Figma may modify, suspend, or discontinue them at any time without notice.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: figma
|
|
3
|
+
description: Implement UIs from Figma URLs or design links by opening Figma in Chrome DevTools MCP, inspecting design frames as a developer, capturing screenshots, and extracting layout, colors, and typography directly from the browser view without relying on rate-limited Figma API MCP tools. Trigger when given a Figma link, frame URL, or design-to-code request.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Figma Browser-Based Inspection (Chrome DevTools MCP Workflow)
|
|
7
|
+
|
|
8
|
+
Use this skill when given a Figma link or frame URL. Rather than using rate-limited Figma API MCP endpoints, open Figma in the browser via Chrome DevTools MCP, inspect the design as a developer, capture screenshots, and translate the visual spec into clean production code.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Required Workflow (Do Not Skip)
|
|
13
|
+
|
|
14
|
+
### 1. Open Figma in Browser via Chrome DevTools MCP
|
|
15
|
+
- Take the provided Figma URL (frame, selection, or node link).
|
|
16
|
+
- Open the URL in Chrome using Chrome DevTools MCP (`open_url` / `navigate_to`).
|
|
17
|
+
- Set viewport width to a comfortable desktop resolution (e.g. `1440x900` or `1920x1080` via `resize_page`).
|
|
18
|
+
- Wait until the Figma canvas and UI fully render.
|
|
19
|
+
|
|
20
|
+
### 2. Capture Visual Reference
|
|
21
|
+
- Take a high-resolution screenshot (`take_screenshot`) of the selected Figma frame or component variant.
|
|
22
|
+
- Save or use this capture as the visual ground truth for implementation.
|
|
23
|
+
|
|
24
|
+
### 3. Developer Inspection in Figma Browser UI
|
|
25
|
+
- Click into the targeted frame/layer or select the Inspect tab / Dev Mode inside the Figma web app using Chrome DevTools MCP (`click` on canvas or panel buttons).
|
|
26
|
+
- Use `evaluate_script` or browser inspection to extract:
|
|
27
|
+
- **Layout & Structure**: Container widths, heights, flex/grid layouts, alignment, gap, padding, and margin.
|
|
28
|
+
- **Color Palette**: Background colors, surface tokens, text colors, border colors, opacity (HEX/RGBA).
|
|
29
|
+
- **Typography**: Font family, font weight (400, 500, 600, 700), font size, line-height, letter-spacing.
|
|
30
|
+
- **Effects**: Box shadows, border-radius, border widths, backdrop blurs.
|
|
31
|
+
|
|
32
|
+
### 4. Translate Design to Code
|
|
33
|
+
- Translate the visual specifications into project conventions:
|
|
34
|
+
- Map colors to the project's existing color tokens or CSS variables.
|
|
35
|
+
- Map layout and typography to the project's design system or utility framework (e.g. Tailwind / CSS modules).
|
|
36
|
+
- Reuse existing UI components (buttons, input fields, badges, icons, modals) rather than duplicating custom markup.
|
|
37
|
+
- If SVG icons or image assets are visible in the design, extract SVG markup or use appropriate project icons.
|
|
38
|
+
|
|
39
|
+
### 5. Validate Visual Parity
|
|
40
|
+
- Run the local app dev server.
|
|
41
|
+
- Open the built page in Chrome DevTools MCP alongside the captured Figma screenshot.
|
|
42
|
+
- Verify visual parity (alignment, spacing, typography, colors) before marking complete.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Operating Guidelines
|
|
47
|
+
|
|
48
|
+
- **No API Quota Limits**: Do NOT call external Figma REST API or rate-limited Figma MCP servers. All inspection is done directly inside the browser using Chrome DevTools MCP.
|
|
49
|
+
- **Visual Fidelity**: Prioritize structural accuracy and component reuse.
|
|
50
|
+
- **Design System First**: Prefer project design system tokens and theme variables over raw hardcoded inline styles whenever project tokens match the design.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
interface:
|
|
2
|
+
display_name: "Figma"
|
|
3
|
+
short_description: "Use Figma MCP for design-to-code work"
|
|
4
|
+
icon_small: "./assets/figma-small.svg"
|
|
5
|
+
icon_large: "./assets/figma.png"
|
|
6
|
+
default_prompt: "Use $figma to inspect the target design and translate it into implementable UI decisions."
|
|
7
|
+
|
|
8
|
+
dependencies:
|
|
9
|
+
tools:
|
|
10
|
+
- type: "mcp"
|
|
11
|
+
value: "figma"
|
|
12
|
+
description: "Figma MCP server"
|
|
13
|
+
transport: "streamable_http"
|
|
14
|
+
url: "https://mcp.figma.com/mcp"
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
|
2
|
+
<path fill="#000" fill-rule="evenodd" d="M4.994 5.986a2.014 2.014 0 1 0 0 4.028h2.069V5.986H4.994Zm5.063-.98h.055a2.014 2.014 0 1 0 0-4.026h-2.07v4.027h2.015Zm1.697.49A2.994 2.994 0 0 0 10.112 0H4.994a2.994 2.994 0 0 0-1.642 5.498A2.99 2.99 0 0 0 2 8a2.99 2.99 0 0 0 1.352 2.503A2.99 2.99 0 0 0 2 13.007C2 14.663 3.358 16 5.008 16c1.665 0 3.035-1.349 3.035-3.02v-2.765a2.984 2.984 0 0 0 2.014.778h.055a2.994 2.994 0 0 0 1.642-5.496Zm-1.642.49h-.055a2.014 2.014 0 1 0 0 4.028h.055a2.014 2.014 0 1 0 0-4.028Zm-7.132 7.02c0-1.111.902-2.013 2.014-2.013h2.069v1.987c0 1.123-.924 2.04-2.055 2.04a2.026 2.026 0 0 1-2.028-2.013Zm4.083-8H4.994a2.014 2.014 0 1 1 0-4.026h2.069v4.027Z" clip-rule="evenodd"/>
|
|
3
|
+
</svg>
|
|
Binary file
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<svg
|
|
2
|
+
width="400"
|
|
3
|
+
height="400"
|
|
4
|
+
viewBox="0 0 400 400"
|
|
5
|
+
fill="none"
|
|
6
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
7
|
+
>
|
|
8
|
+
<path
|
|
9
|
+
d="M97.5 302.5C97.5 274.195 120.445 251.25 148.75 251.25H200V302.5C200 330.805 177.055 353.75 148.75 353.75C120.445 353.75 97.5 330.805 97.5 302.5Z"
|
|
10
|
+
fill="#0ACF83"
|
|
11
|
+
/>
|
|
12
|
+
<path
|
|
13
|
+
d="M200 200C200 171.696 222.945 148.75 251.25 148.75C279.554 148.75 302.5 171.695 302.5 200C302.5 228.305 279.554 251.25 251.25 251.25C222.945 251.25 200 228.304 200 200Z"
|
|
14
|
+
fill="#1ABCFE"
|
|
15
|
+
/>
|
|
16
|
+
<path
|
|
17
|
+
d="M97.5 200C97.5 228.305 120.445 251.25 148.75 251.25H200V148.75H148.75C120.445 148.75 97.5 171.695 97.5 200Z"
|
|
18
|
+
fill="#A259FF"
|
|
19
|
+
/>
|
|
20
|
+
<path
|
|
21
|
+
d="M200 46.25V148.75H251.25C279.555 148.75 302.5 125.805 302.5 97.5C302.5 69.1954 279.555 46.25 251.25 46.25H200Z"
|
|
22
|
+
fill="#FF7262"
|
|
23
|
+
/>
|
|
24
|
+
<path
|
|
25
|
+
d="M97.5 97.5C97.5 125.805 120.445 148.75 148.75 148.75H200V46.25L148.75 46.25C120.445 46.25 97.5 69.1954 97.5 97.5Z"
|
|
26
|
+
fill="#F24E1E"
|
|
27
|
+
/>
|
|
28
|
+
</svg>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Figma Browser Inspection Guide (Chrome DevTools MCP)
|
|
2
|
+
|
|
3
|
+
Guide for inspecting Figma designs directly in Chrome DevTools MCP without using rate-limited API keys.
|
|
4
|
+
|
|
5
|
+
## Browser Navigation Steps
|
|
6
|
+
|
|
7
|
+
1. **Opening Figma Links**:
|
|
8
|
+
Use Chrome DevTools MCP `open_url` with the exact Figma frame URL.
|
|
9
|
+
|
|
10
|
+
2. **Inspecting Canvas & Frames**:
|
|
11
|
+
- Use `resize_page` to set viewport width (e.g. `1440x900`).
|
|
12
|
+
- Use `take_screenshot` to capture the visual representation of the target node.
|
|
13
|
+
- Click on the Inspect panel in Figma to view CSS/properties.
|
|
14
|
+
|
|
15
|
+
3. **Extracting CSS Specifications**:
|
|
16
|
+
- **Backgrounds & Surface**: Note HEX / RGBA values.
|
|
17
|
+
- **Border Radius**: Note border-radius values (`4px`, `8px`, `12px`, `9999px`).
|
|
18
|
+
- **Shadows**: Note box-shadow values (`offset-x`, `offset-y`, `blur`, `spread`, `color`).
|
|
19
|
+
- **Flex/Grid**: Note direction, justify-content, align-items, and gap.
|
|
20
|
+
|
|
21
|
+
4. **Translating to Code**:
|
|
22
|
+
- Map colors to project design system variables (e.g., `--color-primary`, `bg-slate-900`).
|
|
23
|
+
- Map font sizes and weights to project typography scale.
|
|
24
|
+
- Reuse existing component library components (e.g., `<Button>`, `<Card>`, `<Input>`).
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: git-branch-report
|
|
3
|
+
description: Create an on-demand Markdown report for a requested Git branch compared with a base branch. Use when asked for a branch report, branch change summary, diff summary from base, feature branch documentation, or to add a docs file describing what changed in one branch.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Branch Change Report
|
|
7
|
+
|
|
8
|
+
Use this skill when the user asks for a change report for a specific Git branch.
|
|
9
|
+
|
|
10
|
+
## Core Rules
|
|
11
|
+
|
|
12
|
+
- Generate reports only for the branch the user requested.
|
|
13
|
+
- Default base branch is `origin/main` unless the user specifies another base.
|
|
14
|
+
- Do not bulk-generate reports for every branch.
|
|
15
|
+
- Write reports to `docs/branch-change-reports/<safe-branch-name>.md` by default.
|
|
16
|
+
- Keep the report readable; do not include full patch output unless explicitly requested.
|
|
17
|
+
- The report describes committed differences between base and branch. Mention uncommitted changes only if the user asks for current worktree state.
|
|
18
|
+
|
|
19
|
+
## Recommended Workflow
|
|
20
|
+
|
|
21
|
+
1. Resolve inputs.
|
|
22
|
+
- Confirm the requested branch name from the user prompt.
|
|
23
|
+
- Use `origin/main` as the default base.
|
|
24
|
+
- If the branch does not resolve locally, try `origin/<branch>`.
|
|
25
|
+
|
|
26
|
+
2. Generate the report.
|
|
27
|
+
- From the repo root (or skill directory), run:
|
|
28
|
+
`python3 scripts/generate_branch_report.py --branch <branch>`
|
|
29
|
+
- Use `--base <base>` only when the user requested a different base.
|
|
30
|
+
- Use `--output-dir <path>` only when the user requested a different output folder.
|
|
31
|
+
|
|
32
|
+
3. Review the output.
|
|
33
|
+
- Ensure the generated Markdown includes branch/base metadata, ahead/behind counts, unique commits, changed files, diffstat, impact summary, and testing notes.
|
|
34
|
+
- If the helper could not infer enough context, add concise human-readable notes after inspecting relevant changed files.
|
|
35
|
+
|
|
36
|
+
4. Verify.
|
|
37
|
+
- Run `git diff -- docs/branch-change-reports`.
|
|
38
|
+
- Report the generated file path and any verification gaps.
|
|
39
|
+
|
|
40
|
+
## Report Quality
|
|
41
|
+
|
|
42
|
+
- Group changed files by status and impact area.
|
|
43
|
+
- Use commit messages and paths to infer likely product or technical impact.
|
|
44
|
+
- Call out test files touched.
|
|
45
|
+
- If no tests were touched or run, say that explicitly.
|
|
46
|
+
- Avoid claiming behavior that cannot be inferred from commits or source changes.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
interface:
|
|
2
|
+
display_name: "Branch Change Report"
|
|
3
|
+
short_description: "Create branch reports from Git diffs"
|
|
4
|
+
default_prompt: "Use $branch-change-report to generate a Markdown change report for the requested branch against origin/main."
|
|
5
|
+
|
|
6
|
+
policy:
|
|
7
|
+
allow_implicit_invocation: true
|