Setting the file. One moment. Render Stack Card · Agent Skill Stack · github/awesome-copilot · Skills Docs11.10
OpenAI
scripts/render_stack_card.py
Python·157 lines·6 KB
14
15STATUS_COLORS = {
16 "available": ("#0f766e", "#ccfbf1"),
17 "recommended": ("#1d4ed8", "#dbeafe"),
18 "optional": ("#7c3aed", "#ede9fe"),
19 "not-recommended": ("#b45309", "#fef3c7"),
20 "verified": ("#15803d", "#dcfce7"),
21}
22
23
24def clean_text(value: object, limit: int) -> str:
25 text = " ".join(str(value or "").split())
26 return text[:limit]
27
28
29def wrap(value: object, width: int, limit: int) -> list[str]:
30 text = clean_text(value, limit)
31 return textwrap.wrap(text, width=width, break_long_words=False) or [""]
32
33
34def atomic_write(path: Path, content: str) -> None:
35 path.parent.mkdir(parents=True, exist_ok=True)
36 with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=path.parent, delete=False) as handle:
37 handle.write(content)
38 temporary = handle.name
39 os.replace(temporary, path)
40
41
42def validate(payload: object) -> dict[str, object]:
43 if not isinstance(payload, dict):
44 raise ValueError("card input must be a JSON object")
45 if not clean_text(payload.get("title"), 120):
46 raise ValueError("title is required")
47 if not clean_text(payload.get("goal"), 400):
48 raise ValueError("goal is required")
49 skills = payload.get("skills")
50 if not isinstance(skills, list) or not skills:
51 raise ValueError("skills must be a non-empty list")
52 if len(skills) > 8:
53 raise ValueError("a shareable card supports at most 8 Skills")
54 for index, skill in enumerate(skills):
55 if not isinstance(skill, dict) or not clean_text(skill.get("name"), 80):
56 raise ValueError(f"skills[{index}].name is required")
57 return payload
58
59
60def text_element(x: int, y: int, text: object, size: int, color: str, weight: int = 400) -> str:
61 return (
62 f'<text x="{x}" y="{y}" font-family="Inter, ui-sans-serif, system-ui, sans-serif" '
63 f'font-size="{size}" font-weight="{weight}" fill="{color}">{html.escape(str(text))}</text>'
64 )
65
66
67def render(payload: dict[str, object]) -> str:
68 width = 1200
69 title = clean_text(payload.get("title"), 120)
70 goal_lines = wrap(payload.get("goal"), 82, 400)[:3]
71 skills = payload["skills"]
72 warnings = payload.get("boundaries", [])
73 if not isinstance(warnings, list):
74 warnings = [warnings]
75 warning_lines: list[str] = []
76 for warning in warnings[:3]:
77 warning_lines.extend(wrap(warning, 92, 240)[:2])
78 height = 250 + len(goal_lines) * 34 + len(skills) * 92 + max(1, len(warning_lines)) * 30 + 110
79
80 parts = [
81 f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" viewBox="0 0 {width} {height}" role="img" aria-labelledby="title desc">',
82 f'<title id="title">{html.escape(title)}</title>',
83 f'<desc id="desc">{html.escape(clean_text(payload.get("goal"), 400))}</desc>',
84 '<defs><linearGradient id="bg" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#07152f"/><stop offset="1" stop-color="#123b5d"/></linearGradient></defs>',
85 f'<rect width="{width}" height="{height}" rx="36" fill="url(#bg)"/>',
86 '<circle cx="1080" cy="90" r="160" fill="#38bdf8" opacity="0.10"/>',
87 '<circle cx="1120" cy="30" r="80" fill="#a78bfa" opacity="0.12"/>',
88 text_element(64, 72, "AGENT SKILL STACK", 22, "#7dd3fc", 700),
89 text_element(64, 122, title, 38, "#ffffff", 750),
90 ]
91 y = 166
92 for line in goal_lines:
93 parts.append(text_element(64, y, line, 24, "#dbeafe", 400))
94 y += 34
95 y += 22
96
97 for skill in skills:
98 name = clean_text(skill.get("name"), 80)
99 role = clean_text(skill.get("role"), 180)
100 status = clean_text(skill.get("status"), 40).lower() or "recommended"
101 foreground, background = STATUS_COLORS.get(status, ("#334155", "#e2e8f0"))
102 parts.extend([
103 f'<rect x="56" y="{y}" width="1088" height="72" rx="18" fill="#ffffff" opacity="0.96"/>',
104 text_element(84, y + 31, name, 24, "#0f172a", 700),
105 text_element(84, y + 57, role, 18, "#475569", 400),
106 f'<rect x="956" y="{y + 18}" width="160" height="36" rx="18" fill="{background}"/>',
107 text_element(976, y + 43, status.replace("-", " ").title(), 16, foreground, 700),
108 ])
109 y += 92
110
111 parts.append(text_element(64, y + 4, "SAFETY BOUNDARY", 18, "#7dd3fc", 700))
112 y += 34
113 if not warning_lines:
114 warning_lines = ["No additional boundary recorded."]
115 for line in warning_lines:
116 parts.append(text_element(72, y, f"• {line}", 19, "#e2e8f0", 400))
117 y += 30
118
119 verified = clean_text(payload.get("verified"), 40) or "not recorded"
120 footer = clean_text(payload.get("footer"), 120) or "Minimal. Audited. Project-specific."
121 parts.extend([
122 f'<line x1="64" y1="{height - 78}" x2="1136" y2="{height - 78}" stroke="#7dd3fc" opacity="0.25"/>',
123 text_element(64, height - 38, footer, 17, "#bae6fd", 500),
124 text_element(934, height - 38, f"Verified: {verified}", 17, "#bae6fd", 500),
125 "</svg>",
126 ])
127 return "\n".join(parts) + "\n"
128
129
130def main() -> int:
131 parser = argparse.ArgumentParser(description=__doc__)
132 parser.add_argument("--input", required=True, help="JSON card definition")
133 parser.add_argument("--output", required=True, help="SVG destination")
134 parser.add_argument("--force", action="store_true", help="Replace an existing output file")
135 args = parser.parse_args()
136
137 source = Path(args.input).expanduser().resolve()
138 output = Path(args.output).expanduser().resolve()
139 if not source.is_file():
140 raise SystemExit(f"input is not a file: {source}")
141 if output.exists() and not args.force:
142 raise SystemExit(f"refusing to overwrite existing output: {output}")
143 if output.suffix.lower() != ".svg":
144 raise SystemExit("output must use the .svg extension")
145
146 try:
147 payload = validate(json.loads(source.read_text(encoding="utf-8")))
148 svg = render(payload)
149 except (OSError, json.JSONDecodeError, ValueError) as exc:
150 raise SystemExit(str(exc)) from exc
151 atomic_write(output, svg)
152 print(json.dumps({"status": "created", "output": str(output)}, ensure_ascii=False))
153 return 0
154
155
156if __name__ == "__main__":
157 raise SystemExit(main())