@veyralabs/skills 0.1.0 → 0.2.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/bin/cli.js +66 -41
- package/package.json +3 -3
- package/skills/webcloner/SKILL.md +565 -0
- package/skills/webcloner/references/animation-playbook.md +292 -0
- package/skills/webcloner/references/behavior-spec-format.md +259 -0
- package/skills/webcloner/references/component-detection.md +209 -0
- package/skills/webcloner/references/stack-presets.md +328 -0
- package/skills/webcloner/scripts/compare.mjs +87 -0
- package/skills/webcloner/scripts/download-assets.mjs +160 -0
- package/skills/webcloner/scripts/extract.py +344 -0
- package/validate.js +14 -4
- /package/skills/{brandaudit → naming-suite/brandaudit}/SKILL.md +0 -0
- /package/skills/{brandaudit → naming-suite/brandaudit}/references/audit-framework.md +0 -0
- /package/skills/{brandaudit → naming-suite/brandaudit}/references/examples/sample-audits.md +0 -0
- /package/skills/{brandaudit → naming-suite/brandaudit}/references/rebrand-decisions.md +0 -0
- /package/skills/{brandaudit → naming-suite/brandaudit}/references/weakness-patterns.md +0 -0
- /package/skills/{competitornames → naming-suite/competitornames}/SKILL.md +0 -0
- /package/skills/{competitornames → naming-suite/competitornames}/references/examples/sample-analyses.md +0 -0
- /package/skills/{competitornames → naming-suite/competitornames}/references/pattern-analysis.md +0 -0
- /package/skills/{competitornames → naming-suite/competitornames}/references/whitespace-mapping.md +0 -0
- /package/skills/{domainforge → naming-suite/domainforge}/SKILL.md +0 -0
- /package/skills/{domainforge → naming-suite/domainforge}/references/brand-archetypes.md +0 -0
- /package/skills/{domainforge → naming-suite/domainforge}/references/examples/sample-outputs.md +0 -0
- /package/skills/{domainforge → naming-suite/domainforge}/references/naming-patterns.md +0 -0
- /package/skills/{domainforge → naming-suite/domainforge}/references/scoring-rubric.md +0 -0
- /package/skills/{domainforge → naming-suite/domainforge}/references/tld-strategy.md +0 -0
- /package/skills/{namingguide → naming-suite/namingguide}/SKILL.md +0 -0
- /package/skills/{namingguide → naming-suite/namingguide}/references/examples/sample-guides.md +0 -0
- /package/skills/{namingguide → naming-suite/namingguide}/references/guide-structure.md +0 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* download-assets.mjs — Download all assets from site-manifest.json to public/
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node scripts/download-assets.mjs docs/site-manifest.json public/
|
|
7
|
+
*
|
|
8
|
+
* Requires: sharp (optional, for WebP conversion)
|
|
9
|
+
* npm install sharp
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from 'fs';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import { pipeline } from 'stream/promises';
|
|
15
|
+
import { createWriteStream, mkdirSync } from 'fs';
|
|
16
|
+
|
|
17
|
+
const [,, manifestPath = 'docs/site-manifest.json', outputDir = 'public'] = process.argv;
|
|
18
|
+
|
|
19
|
+
if (!fs.existsSync(manifestPath)) {
|
|
20
|
+
console.error(`Manifest not found: ${manifestPath}`);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
25
|
+
const baseUrl = manifest.url;
|
|
26
|
+
|
|
27
|
+
let sharp;
|
|
28
|
+
try {
|
|
29
|
+
sharp = (await import('sharp')).default;
|
|
30
|
+
} catch {
|
|
31
|
+
console.warn('sharp not installed — skipping WebP conversion (npm install sharp to enable)');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function sanitizePath(url, type) {
|
|
35
|
+
try {
|
|
36
|
+
const u = new URL(url);
|
|
37
|
+
const ext = path.extname(u.pathname).toLowerCase() || '.bin';
|
|
38
|
+
const name = path.basename(u.pathname, ext) || 'asset';
|
|
39
|
+
const safe = name.replace(/[^a-z0-9-_]/gi, '-').slice(0, 60);
|
|
40
|
+
return path.join(outputDir, type, `${safe}${ext}`);
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function fetchAsset(url) {
|
|
47
|
+
const resolved = url.startsWith('http') ? url : new URL(url, baseUrl).href;
|
|
48
|
+
const res = await fetch(resolved, {
|
|
49
|
+
headers: { 'User-Agent': 'Mozilla/5.0 (compatible; WebCloner/1.0)' },
|
|
50
|
+
redirect: 'follow',
|
|
51
|
+
});
|
|
52
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}: ${resolved}`);
|
|
53
|
+
return { res, resolved };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function downloadImage(url) {
|
|
57
|
+
const destPath = sanitizePath(url, 'images');
|
|
58
|
+
if (!destPath) return null;
|
|
59
|
+
|
|
60
|
+
const ext = path.extname(destPath).toLowerCase();
|
|
61
|
+
const isConvertible = ['.jpg', '.jpeg', '.png'].includes(ext) && sharp;
|
|
62
|
+
const finalPath = isConvertible ? destPath.replace(ext, '.webp') : destPath;
|
|
63
|
+
|
|
64
|
+
if (fs.existsSync(finalPath)) return finalPath;
|
|
65
|
+
mkdirSync(path.dirname(finalPath), { recursive: true });
|
|
66
|
+
|
|
67
|
+
const { res } = await fetchAsset(url);
|
|
68
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
69
|
+
|
|
70
|
+
if (isConvertible) {
|
|
71
|
+
await sharp(buffer).webp({ quality: 85 }).toFile(finalPath);
|
|
72
|
+
} else {
|
|
73
|
+
fs.writeFileSync(finalPath, buffer);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return finalPath;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function downloadGeneric(url, type) {
|
|
80
|
+
const destPath = sanitizePath(url, type);
|
|
81
|
+
if (!destPath || fs.existsSync(destPath)) return destPath;
|
|
82
|
+
mkdirSync(path.dirname(destPath), { recursive: true });
|
|
83
|
+
|
|
84
|
+
const { res } = await fetchAsset(url);
|
|
85
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
86
|
+
fs.writeFileSync(destPath, buffer);
|
|
87
|
+
return destPath;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function batch(items, fn, concurrency = 4) {
|
|
91
|
+
const results = [];
|
|
92
|
+
for (let i = 0; i < items.length; i += concurrency) {
|
|
93
|
+
const chunk = items.slice(i, i + concurrency);
|
|
94
|
+
const settled = await Promise.allSettled(chunk.map(fn));
|
|
95
|
+
for (const r of settled) {
|
|
96
|
+
if (r.status === 'fulfilled') results.push({ ok: true, path: r.value });
|
|
97
|
+
else results.push({ ok: false, error: r.reason?.message });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return results;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const assets = manifest.assets || {};
|
|
104
|
+
const report = { images: [], videos: [], fonts: [], failed: [] };
|
|
105
|
+
|
|
106
|
+
console.log('Downloading assets...\n');
|
|
107
|
+
|
|
108
|
+
// Images
|
|
109
|
+
if (assets.images?.length) {
|
|
110
|
+
console.log(`Images: ${assets.images.length}`);
|
|
111
|
+
const urls = [...new Set(assets.images.map(i => i.src).filter(Boolean))];
|
|
112
|
+
const results = await batch(urls, downloadImage);
|
|
113
|
+
results.forEach((r, i) => {
|
|
114
|
+
if (r.ok) { report.images.push(r.path); process.stdout.write('.'); }
|
|
115
|
+
else { report.failed.push({ url: urls[i], error: r.error }); process.stdout.write('x'); }
|
|
116
|
+
});
|
|
117
|
+
console.log();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Videos
|
|
121
|
+
if (assets.videos?.length) {
|
|
122
|
+
console.log(`\nVideos: ${assets.videos.length}`);
|
|
123
|
+
const urls = [...new Set(assets.videos.map(v => v.src).filter(Boolean))];
|
|
124
|
+
const results = await batch(urls, u => downloadGeneric(u, 'videos'), 2);
|
|
125
|
+
results.forEach((r, i) => {
|
|
126
|
+
if (r.ok) { report.videos.push(r.path); process.stdout.write('.'); }
|
|
127
|
+
else { report.failed.push({ url: urls[i], error: r.error }); process.stdout.write('x'); }
|
|
128
|
+
});
|
|
129
|
+
console.log();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Self-hosted fonts
|
|
133
|
+
if (assets.fonts?.length) {
|
|
134
|
+
const selfHosted = assets.fonts.filter(f => !f.includes('googleapis') && !f.includes('typekit'));
|
|
135
|
+
if (selfHosted.length) {
|
|
136
|
+
console.log(`\nFonts (self-hosted): ${selfHosted.length}`);
|
|
137
|
+
const results = await batch(selfHosted, u => downloadGeneric(u, 'fonts'));
|
|
138
|
+
results.forEach((r, i) => {
|
|
139
|
+
if (r.ok) { report.fonts.push(r.path); process.stdout.write('.'); }
|
|
140
|
+
else { report.failed.push({ url: selfHosted[i], error: r.error }); process.stdout.write('x'); }
|
|
141
|
+
});
|
|
142
|
+
console.log();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Write report
|
|
147
|
+
const reportPath = 'docs/assets-report.json';
|
|
148
|
+
mkdirSync('docs', { recursive: true });
|
|
149
|
+
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
|
|
150
|
+
|
|
151
|
+
console.log(`\n--- Download Report ---`);
|
|
152
|
+
console.log(`Images: ${report.images.length}`);
|
|
153
|
+
console.log(`Videos: ${report.videos.length}`);
|
|
154
|
+
console.log(`Fonts: ${report.fonts.length}`);
|
|
155
|
+
console.log(`Failed: ${report.failed.length}`);
|
|
156
|
+
if (report.failed.length) {
|
|
157
|
+
console.log('\nFailed assets:');
|
|
158
|
+
report.failed.forEach(f => console.log(` ✗ ${f.url}\n ${f.error}`));
|
|
159
|
+
}
|
|
160
|
+
console.log(`\nReport: ${reportPath}`);
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
extract.py — Site manifest extractor using Scrapling.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
python scripts/extract.py <url> [--output path/to/manifest.json] [--section ".selector"]
|
|
7
|
+
|
|
8
|
+
Output: JSON manifest with DOM structure, computed CSS, assets, animations, and tech stack.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import json
|
|
13
|
+
import re
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from urllib.parse import urljoin, urlparse
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
from scrapling import AsyncPlaywrightFetcher
|
|
20
|
+
except ImportError:
|
|
21
|
+
print("ERROR: scrapling not installed. Run: pip install scrapling && scrapling install")
|
|
22
|
+
sys.exit(1)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# CSS properties to extract for each significant element
|
|
26
|
+
CSS_PROPERTIES = [
|
|
27
|
+
"display", "position", "top", "right", "bottom", "left", "z-index",
|
|
28
|
+
"width", "min-width", "max-width", "height", "min-height", "max-height",
|
|
29
|
+
"margin", "margin-top", "margin-right", "margin-bottom", "margin-left",
|
|
30
|
+
"padding", "padding-top", "padding-right", "padding-bottom", "padding-left",
|
|
31
|
+
"flex-direction", "flex-wrap", "justify-content", "align-items", "align-self",
|
|
32
|
+
"flex", "flex-grow", "flex-shrink", "flex-basis", "gap", "row-gap", "column-gap",
|
|
33
|
+
"grid-template-columns", "grid-template-rows", "grid-column", "grid-row",
|
|
34
|
+
"font-family", "font-size", "font-weight", "font-style", "line-height",
|
|
35
|
+
"letter-spacing", "text-align", "text-transform", "text-decoration", "color",
|
|
36
|
+
"background-color", "background-image", "background-size", "background-position",
|
|
37
|
+
"border", "border-radius", "border-color", "border-width", "border-style",
|
|
38
|
+
"box-shadow", "opacity", "overflow", "overflow-x", "overflow-y",
|
|
39
|
+
"transform", "transition", "animation", "animation-name", "animation-duration",
|
|
40
|
+
"cursor", "pointer-events", "user-select",
|
|
41
|
+
"object-fit", "object-position", "aspect-ratio",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
ANIMATION_SIGNATURES = {
|
|
46
|
+
"gsap": [
|
|
47
|
+
"window.gsap !== undefined",
|
|
48
|
+
"window.ScrollTrigger !== undefined",
|
|
49
|
+
"window.ScrollSmoother !== undefined",
|
|
50
|
+
],
|
|
51
|
+
"framer-motion": [
|
|
52
|
+
"document.querySelector('[data-framer-appear]') !== null",
|
|
53
|
+
"document.querySelector('[data-projection-id]') !== null",
|
|
54
|
+
],
|
|
55
|
+
"lenis": [
|
|
56
|
+
"window.lenis !== undefined",
|
|
57
|
+
"document.documentElement.classList.contains('lenis')",
|
|
58
|
+
"document.querySelector('.lenis') !== null",
|
|
59
|
+
],
|
|
60
|
+
"aos": [
|
|
61
|
+
"document.querySelector('[data-aos]') !== null",
|
|
62
|
+
],
|
|
63
|
+
"swiper": [
|
|
64
|
+
"window.Swiper !== undefined",
|
|
65
|
+
"document.querySelector('.swiper') !== null",
|
|
66
|
+
],
|
|
67
|
+
"locomotive": [
|
|
68
|
+
"document.querySelector('[data-scroll-container]') !== null",
|
|
69
|
+
"window.LocomotiveScroll !== undefined",
|
|
70
|
+
],
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
EXTRACTION_SCRIPT = """
|
|
75
|
+
() => {
|
|
76
|
+
const CSS_PROPS = %s;
|
|
77
|
+
|
|
78
|
+
function getComputedStyleMap(el) {
|
|
79
|
+
const cs = window.getComputedStyle(el);
|
|
80
|
+
const result = {};
|
|
81
|
+
for (const prop of CSS_PROPS) {
|
|
82
|
+
const val = cs.getPropertyValue(prop);
|
|
83
|
+
if (val && val !== 'none' && val !== 'normal' && val !== 'auto' && val !== '0px' && val !== 'rgba(0, 0, 0, 0)') {
|
|
84
|
+
result[prop] = val;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function extractElement(el, depth) {
|
|
91
|
+
if (depth > 4) return null;
|
|
92
|
+
const rect = el.getBoundingClientRect();
|
|
93
|
+
if (rect.width === 0 && rect.height === 0) return null;
|
|
94
|
+
|
|
95
|
+
const result = {
|
|
96
|
+
tag: el.tagName.toLowerCase(),
|
|
97
|
+
id: el.id || null,
|
|
98
|
+
classes: Array.from(el.classList).slice(0, 10),
|
|
99
|
+
text: el.childNodes.length === 1 && el.childNodes[0].nodeType === 3
|
|
100
|
+
? el.textContent.trim().slice(0, 200)
|
|
101
|
+
: null,
|
|
102
|
+
href: el.tagName === 'A' ? el.href : null,
|
|
103
|
+
src: el.tagName === 'IMG' ? el.src : null,
|
|
104
|
+
alt: el.tagName === 'IMG' ? el.alt : null,
|
|
105
|
+
styles: getComputedStyleMap(el),
|
|
106
|
+
bounds: {
|
|
107
|
+
top: Math.round(rect.top + window.scrollY),
|
|
108
|
+
left: Math.round(rect.left),
|
|
109
|
+
width: Math.round(rect.width),
|
|
110
|
+
height: Math.round(rect.height),
|
|
111
|
+
},
|
|
112
|
+
children: [],
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
if (depth < 3) {
|
|
116
|
+
for (const child of el.children) {
|
|
117
|
+
const childData = extractElement(child, depth + 1);
|
|
118
|
+
if (childData) result.children.push(childData);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Extract sections (top-level structural blocks)
|
|
126
|
+
const sectionSelectors = [
|
|
127
|
+
'header', 'nav', 'main', 'footer', 'section', 'article',
|
|
128
|
+
'[class*="section"]', '[class*="hero"]', '[class*="banner"]',
|
|
129
|
+
'[id*="section"]', '[id*="hero"]',
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
const seen = new Set();
|
|
133
|
+
const sections = [];
|
|
134
|
+
|
|
135
|
+
for (const sel of sectionSelectors) {
|
|
136
|
+
for (const el of document.querySelectorAll(sel)) {
|
|
137
|
+
if (seen.has(el)) continue;
|
|
138
|
+
seen.add(el);
|
|
139
|
+
const data = extractElement(el, 0);
|
|
140
|
+
if (data) sections.push(data);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Color palette from all computed background-colors
|
|
145
|
+
const colors = new Set();
|
|
146
|
+
document.querySelectorAll('*').forEach(el => {
|
|
147
|
+
const cs = window.getComputedStyle(el);
|
|
148
|
+
const bg = cs.backgroundColor;
|
|
149
|
+
const color = cs.color;
|
|
150
|
+
if (bg && bg !== 'rgba(0, 0, 0, 0)') colors.add(bg);
|
|
151
|
+
if (color && color !== 'rgba(0, 0, 0, 0)') colors.add(color);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Typography from headings and body text
|
|
155
|
+
const typography = [];
|
|
156
|
+
['h1','h2','h3','h4','p','a','span','li'].forEach(tag => {
|
|
157
|
+
const el = document.querySelector(tag);
|
|
158
|
+
if (el) {
|
|
159
|
+
const cs = window.getComputedStyle(el);
|
|
160
|
+
typography.push({
|
|
161
|
+
tag,
|
|
162
|
+
fontFamily: cs.fontFamily,
|
|
163
|
+
fontSize: cs.fontSize,
|
|
164
|
+
fontWeight: cs.fontWeight,
|
|
165
|
+
lineHeight: cs.lineHeight,
|
|
166
|
+
letterSpacing: cs.letterSpacing,
|
|
167
|
+
color: cs.color,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Asset inventory
|
|
173
|
+
const images = Array.from(document.querySelectorAll('img')).map(img => ({
|
|
174
|
+
src: img.src,
|
|
175
|
+
alt: img.alt,
|
|
176
|
+
width: img.naturalWidth || img.width,
|
|
177
|
+
height: img.naturalHeight || img.height,
|
|
178
|
+
lazy: img.loading === 'lazy',
|
|
179
|
+
}));
|
|
180
|
+
|
|
181
|
+
const videos = Array.from(document.querySelectorAll('video')).map(v => ({
|
|
182
|
+
src: v.src || (v.querySelector('source') ? v.querySelector('source').src : null),
|
|
183
|
+
poster: v.poster,
|
|
184
|
+
autoplay: v.autoplay,
|
|
185
|
+
loop: v.loop,
|
|
186
|
+
muted: v.muted,
|
|
187
|
+
}));
|
|
188
|
+
|
|
189
|
+
const fonts = Array.from(document.querySelectorAll('link[rel="stylesheet"]'))
|
|
190
|
+
.map(l => l.href)
|
|
191
|
+
.filter(h => h.includes('fonts.googleapis') || h.includes('typekit') || h.includes('fonts.adobe'));
|
|
192
|
+
|
|
193
|
+
const svgs = Array.from(document.querySelectorAll('svg')).map((svg, i) => ({
|
|
194
|
+
id: svg.id || `svg-${i}`,
|
|
195
|
+
viewBox: svg.getAttribute('viewBox'),
|
|
196
|
+
html: svg.outerHTML.slice(0, 500),
|
|
197
|
+
}));
|
|
198
|
+
|
|
199
|
+
return { sections, colorPalette: Array.from(colors), typography, assets: { images, videos, fonts, svgs } };
|
|
200
|
+
}
|
|
201
|
+
""" % json.dumps(CSS_PROPERTIES)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
async def detect_animations(page):
|
|
205
|
+
detected = []
|
|
206
|
+
for lib, checks in ANIMATION_SIGNATURES.items():
|
|
207
|
+
for check in checks:
|
|
208
|
+
try:
|
|
209
|
+
result = await page.evaluate(f"() => {{ try {{ return Boolean({check}); }} catch(e) {{ return false; }} }}")
|
|
210
|
+
if result:
|
|
211
|
+
detected.append(lib)
|
|
212
|
+
break
|
|
213
|
+
except Exception:
|
|
214
|
+
pass
|
|
215
|
+
return list(set(detected))
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
async def detect_tech_stack(page):
|
|
219
|
+
stack = {}
|
|
220
|
+
|
|
221
|
+
checks = {
|
|
222
|
+
"react": "window.__NEXT_DATA__ !== undefined || window.React !== undefined",
|
|
223
|
+
"nextjs": "window.__NEXT_DATA__ !== undefined",
|
|
224
|
+
"vue": "window.__VUE__ !== undefined || document.querySelector('[data-v-app]') !== null",
|
|
225
|
+
"nuxt": "window.__NUXT__ !== undefined",
|
|
226
|
+
"svelte": "document.querySelector('[data-svelte]') !== null",
|
|
227
|
+
"webflow": "document.querySelector('html[data-wf-site]') !== null",
|
|
228
|
+
"framer": "document.querySelector('html[data-framer-hydrate-v2]') !== null",
|
|
229
|
+
"shopify": "window.Shopify !== undefined",
|
|
230
|
+
"wordpress": "document.querySelector('meta[name=\"generator\"][content^=\"WordPress\"]') !== null",
|
|
231
|
+
"tailwind": "Array.from(document.querySelectorAll('*')).some(el => Array.from(el.classList).some(c => /^(flex|grid|p-|m-|text-|bg-|w-|h-)/.test(c)))",
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
for name, check in checks.items():
|
|
235
|
+
try:
|
|
236
|
+
result = await page.evaluate(f"() => {{ try {{ return Boolean({check}); }} catch(e) {{ return false; }} }}")
|
|
237
|
+
if result:
|
|
238
|
+
stack[name] = True
|
|
239
|
+
except Exception:
|
|
240
|
+
pass
|
|
241
|
+
|
|
242
|
+
return stack
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
async def extract(url, output_path, section_selector=None):
|
|
246
|
+
print(f"Extracting: {url}")
|
|
247
|
+
|
|
248
|
+
fetcher = AsyncPlaywrightFetcher(headless=True)
|
|
249
|
+
|
|
250
|
+
page_data = await fetcher.async_fetch(
|
|
251
|
+
url,
|
|
252
|
+
wait_selector="body",
|
|
253
|
+
wait_for="networkidle",
|
|
254
|
+
page_action=None,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
# Use the underlying Playwright page for JS evaluation
|
|
258
|
+
async with fetcher._get_browser_context() as context:
|
|
259
|
+
page = await context.new_page()
|
|
260
|
+
await page.goto(url, wait_until="networkidle")
|
|
261
|
+
await page.wait_for_timeout(2000) # let animations settle
|
|
262
|
+
|
|
263
|
+
# Scroll to trigger lazy loads
|
|
264
|
+
await page.evaluate("""
|
|
265
|
+
async () => {
|
|
266
|
+
const height = document.body.scrollHeight;
|
|
267
|
+
for (let y = 0; y < height; y += 200) {
|
|
268
|
+
window.scrollTo(0, y);
|
|
269
|
+
await new Promise(r => setTimeout(r, 50));
|
|
270
|
+
}
|
|
271
|
+
window.scrollTo(0, 0);
|
|
272
|
+
await new Promise(r => setTimeout(r, 500));
|
|
273
|
+
}
|
|
274
|
+
""")
|
|
275
|
+
|
|
276
|
+
animations = await detect_animations(page)
|
|
277
|
+
tech_stack = await detect_tech_stack(page)
|
|
278
|
+
dom_data = await page.evaluate(EXTRACTION_SCRIPT)
|
|
279
|
+
|
|
280
|
+
# Get page title and meta
|
|
281
|
+
title = await page.title()
|
|
282
|
+
meta_desc = await page.evaluate(
|
|
283
|
+
"() => document.querySelector('meta[name=\"description\"]')?.content || ''"
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# Detect breakpoints from stylesheets
|
|
287
|
+
breakpoints = await page.evaluate("""
|
|
288
|
+
() => {
|
|
289
|
+
const bps = new Set();
|
|
290
|
+
for (const sheet of document.styleSheets) {
|
|
291
|
+
try {
|
|
292
|
+
for (const rule of sheet.cssRules) {
|
|
293
|
+
if (rule.type === CSSRule.MEDIA_RULE) {
|
|
294
|
+
const match = rule.conditionText.match(/\\d+/g);
|
|
295
|
+
if (match) match.forEach(n => bps.add(parseInt(n)));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
} catch(e) {}
|
|
299
|
+
}
|
|
300
|
+
return Array.from(bps).sort((a,b) => a-b);
|
|
301
|
+
}
|
|
302
|
+
""")
|
|
303
|
+
|
|
304
|
+
manifest = {
|
|
305
|
+
"url": url,
|
|
306
|
+
"title": title,
|
|
307
|
+
"description": meta_desc,
|
|
308
|
+
"techStack": tech_stack,
|
|
309
|
+
"animations": {
|
|
310
|
+
"libraries": animations,
|
|
311
|
+
},
|
|
312
|
+
"breakpoints": breakpoints,
|
|
313
|
+
"colorPalette": dom_data["colorPalette"][:30],
|
|
314
|
+
"typography": dom_data["typography"],
|
|
315
|
+
"sections": dom_data["sections"],
|
|
316
|
+
"assets": dom_data["assets"],
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
output_file = Path(output_path)
|
|
320
|
+
output_file.parent.mkdir(parents=True, exist_ok=True)
|
|
321
|
+
output_file.write_text(json.dumps(manifest, indent=2, ensure_ascii=False))
|
|
322
|
+
|
|
323
|
+
print(f"\nManifest written to: {output_path}")
|
|
324
|
+
print(f" Sections found: {len(manifest['sections'])}")
|
|
325
|
+
print(f" Images found: {len(manifest['assets']['images'])}")
|
|
326
|
+
print(f" SVGs found: {len(manifest['assets']['svgs'])}")
|
|
327
|
+
print(f" Animation libs: {', '.join(animations) or 'none detected'}")
|
|
328
|
+
print(f" Tech stack: {', '.join(k for k,v in tech_stack.items() if v)}")
|
|
329
|
+
print(f" Breakpoints: {breakpoints}")
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def main():
|
|
333
|
+
import argparse
|
|
334
|
+
parser = argparse.ArgumentParser(description="Extract site manifest")
|
|
335
|
+
parser.add_argument("url", help="Target URL")
|
|
336
|
+
parser.add_argument("--output", default="docs/site-manifest.json", help="Output path")
|
|
337
|
+
parser.add_argument("--section", default=None, help="CSS selector to scope extraction")
|
|
338
|
+
args = parser.parse_args()
|
|
339
|
+
|
|
340
|
+
asyncio.run(extract(args.url, args.output, args.section))
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
if __name__ == "__main__":
|
|
344
|
+
main()
|
package/validate.js
CHANGED
|
@@ -11,13 +11,24 @@ const SKILLS_DIR = path.join(__dirname, 'skills');
|
|
|
11
11
|
|
|
12
12
|
function findSkillFiles(dir) {
|
|
13
13
|
const results = [];
|
|
14
|
-
|
|
15
14
|
if (!fs.existsSync(dir)) return results;
|
|
16
15
|
|
|
17
16
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
18
17
|
if (!entry.isDirectory()) continue;
|
|
19
|
-
const
|
|
20
|
-
|
|
18
|
+
const entryPath = path.join(dir, entry.name);
|
|
19
|
+
const direct = path.join(entryPath, 'SKILL.md');
|
|
20
|
+
|
|
21
|
+
if (fs.existsSync(direct)) {
|
|
22
|
+
// Standalone skill
|
|
23
|
+
results.push(direct);
|
|
24
|
+
} else {
|
|
25
|
+
// Pack folder — scan one level deeper
|
|
26
|
+
for (const sub of fs.readdirSync(entryPath, { withFileTypes: true })) {
|
|
27
|
+
if (!sub.isDirectory()) continue;
|
|
28
|
+
const candidate = path.join(entryPath, sub.name, 'SKILL.md');
|
|
29
|
+
if (fs.existsSync(candidate)) results.push(candidate);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
21
32
|
}
|
|
22
33
|
|
|
23
34
|
return results;
|
|
@@ -45,7 +56,6 @@ function parseFrontmatter(content) {
|
|
|
45
56
|
|
|
46
57
|
if (!key || key.includes(' ')) { i++; continue; }
|
|
47
58
|
|
|
48
|
-
// Handle block scalars (> and |)
|
|
49
59
|
if (rawValue === '>' || rawValue === '|') {
|
|
50
60
|
const blockLines = [];
|
|
51
61
|
i++;
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/skills/{competitornames → naming-suite/competitornames}/references/pattern-analysis.md
RENAMED
|
File without changes
|
/package/skills/{competitornames → naming-suite/competitornames}/references/whitespace-mapping.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/skills/{domainforge → naming-suite/domainforge}/references/examples/sample-outputs.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/skills/{namingguide → naming-suite/namingguide}/references/examples/sample-guides.md
RENAMED
|
File without changes
|
|
File without changes
|