Skip to content
SlashClone

Show "works with" app logos and filter by app

@slashclone/integration-logos

Add a "works with" feature to a catalog or marketplace: each item shows logos of the apps it integrates with, and people can browse and filter search by app. Tags are detected from strong signals like env vars, hosts, and MCP servers before mentions, owners can override them, and logos come from simple-icons with a lettered fallback, an SVG safety check, and a dry-run retag job.

SkillOtherv0.1.0Tested
Activity
New: be the first to try it
  1. 1#!/usr/bin/env node
  2. 2// Checks hand-supplied logo SVGs before you serve them from your own origin, where an
  3. 3// SVG opened directly can run script. Node built-ins only.
  4. 4// Usage: node scripts/check-svg.mjs <file.svg | folder> [...] [--report out/svg-check.json]
  5. 5// Exits 1 when any file fails, unless --report-only is given (then the report says so).
  6. 6import { mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
  7. 7import { dirname, join } from "node:path";
  8. 8import { parseArgs } from "node:util";
  9. 9
  10. 10const MAX_BYTES = 10_000;
  11. 11const RULES = [
  12. 12 [/<script\b/i, "has a script element"],
  13. 13 [/<foreignObject\b/i, "has foreignObject"],
  14. 14 [/<(iframe|embed|object)\b/i, "embeds another document"],
  15. 15 [/<image\b/i, "embeds an image"],
  16. 16 [/\son[a-z]+\s*=/i, "has an event handler attribute"],
  17. 17 [/javascript:|data:text\/html/i, "has a script URL"],
  18. 18 [/(?:xlink:)?href\s*=\s*["'](?!#)/i, "links outside the file"],
  19. 19 [/url\(\s*["']?(?!#)/i, "loads a URL from CSS"],
  20. 20 [/@import\b/i, "imports a stylesheet"],
  21. 21 [/<!ENTITY\b|<!DOCTYPE\b/i, "declares a DOCTYPE or entity"],
  22. 22];
  23. 24const { values, positionals } = parseArgs({
  24. 25 allowPositionals: true,
  25. 26 options: {
  26. 27 report: { type: "string" },
  27. 28 "report-only": { type: "boolean", default: false },
  28. 29 },
  29. 30});
  30. 31if (positionals.length === 0) {
  31. 32 console.error("Give one or more SVG files or folders.");
  32. 33 process.exit(2);
  33. 36const files = [];
  34. 37for (const p of positionals) {
  35. 38 if (statSync(p).isDirectory()) {
  36. 39 for (const f of readdirSync(p).sort()) if (f.endsWith(".svg")) files.push(join(p, f));
  37. 40 } else files.push(p);
  38. 43const results = files.map((file) => {
  39. 44 const svg = readFileSync(file, "utf8");
  40. 45 const problems = [];
  41. 46 if (!/<svg\b/i.test(svg)) problems.push("is not an SVG");
  42. 47 if (Buffer.byteLength(svg) > MAX_BYTES) problems.push(`is over ${MAX_BYTES} bytes`);
  43. 48 for (const [re, why] of RULES) if (re.test(svg)) problems.push(why);
  44. 49 const warnings = [];
  45. 50 const vb = /viewBox\s*=\s*["']\s*[-\d.]+[\s,]+[-\d.]+[\s,]+([\d.]+)[\s,]+([\d.]+)/i.exec(svg);
  46. 51 if (!vb) warnings.push("has no viewBox");
  47. 52 else if (vb[1] !== vb[2]) warnings.push("viewBox is not square");
  48. 53 return { file, ok: problems.length === 0, problems, warnings };
  49. 54});
  50. 56const ok = results.every((r) => r.ok);
  51. 57for (const r of results) {
  52. 58 const notes = [...r.problems, ...r.warnings.map((w) => `warning: ${w}`)];
  53. 59 console.log(`${r.ok ? "ok " : "FAIL"} ${r.file}${notes.length ? ` (${notes.join("; ")})` : ""}`);
  54. 61if (values.report) {
  55. 62 mkdirSync(dirname(values.report), { recursive: true });
  56. 63 writeFileSync(values.report, `${JSON.stringify({ ok, results }, null, 2)}\n`);
  57. 65process.exit(ok || values["report-only"] ? 0 : 1);

Cookies that keep you signed in and remember these choices are always on. Everything else is off until you turn it on.