similarbuild 0.1.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.
Files changed (76) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/LICENSE +21 -0
  3. package/README.md +301 -0
  4. package/bin/install.js +256 -0
  5. package/lib/copy-templates.mjs +52 -0
  6. package/lib/install-deps.mjs +62 -0
  7. package/lib/prompt-config.mjs +83 -0
  8. package/lib/verify-env.mjs +19 -0
  9. package/package.json +63 -0
  10. package/scripts/sync-templates.mjs +71 -0
  11. package/templates/commands/build-page.md +490 -0
  12. package/templates/commands/build-site.md +548 -0
  13. package/templates/commands/clip-section.md +519 -0
  14. package/templates/memory/anti-patterns.md +212 -0
  15. package/templates/memory/design-knowledge.md +225 -0
  16. package/templates/memory/fixes.md +163 -0
  17. package/templates/memory/patterns.md +681 -0
  18. package/templates/presets/shopify-section.yaml +51 -0
  19. package/templates/presets/wp-elementor.yaml +49 -0
  20. package/templates/reports/fixtures/mock-run-1.json +115 -0
  21. package/templates/reports/fixtures/mock-run-2.json +72 -0
  22. package/templates/reports/report-renderer.mjs +218 -0
  23. package/templates/reports/report-template.html +571 -0
  24. package/templates/skills/sb-build-shopify/SKILL.md +104 -0
  25. package/templates/skills/sb-build-shopify/references/shopify-build-rules.md +563 -0
  26. package/templates/skills/sb-build-shopify/scripts/build-shopify.mjs +637 -0
  27. package/templates/skills/sb-build-shopify/scripts/tests/test-build-shopify.mjs +424 -0
  28. package/templates/skills/sb-build-wp/SKILL.md +83 -0
  29. package/templates/skills/sb-build-wp/references/wp-build-rules.md +376 -0
  30. package/templates/skills/sb-build-wp/scripts/build-wp.mjs +327 -0
  31. package/templates/skills/sb-build-wp/scripts/tests/test-build-wp.mjs +224 -0
  32. package/templates/skills/sb-compare-visual/SKILL.md +121 -0
  33. package/templates/skills/sb-compare-visual/scripts/compare-visual.mjs +387 -0
  34. package/templates/skills/sb-compare-visual/scripts/lib/compare-tokens.mjs +273 -0
  35. package/templates/skills/sb-compare-visual/scripts/tests/test-compare-tokens.mjs +350 -0
  36. package/templates/skills/sb-compare-visual/scripts/tests/test-compare-visual.mjs +626 -0
  37. package/templates/skills/sb-crawl-and-list/SKILL.md +99 -0
  38. package/templates/skills/sb-crawl-and-list/scripts/crawl-and-list.mjs +437 -0
  39. package/templates/skills/sb-crawl-and-list/scripts/lib/blocklist-filter.mjs +176 -0
  40. package/templates/skills/sb-crawl-and-list/scripts/lib/fallback-crawler.mjs +107 -0
  41. package/templates/skills/sb-crawl-and-list/scripts/lib/page-classifier.mjs +89 -0
  42. package/templates/skills/sb-crawl-and-list/scripts/lib/sitemap-parser.mjs +118 -0
  43. package/templates/skills/sb-crawl-and-list/scripts/tests/test-blocklist-filter.mjs +204 -0
  44. package/templates/skills/sb-crawl-and-list/scripts/tests/test-crawl-and-list.mjs +276 -0
  45. package/templates/skills/sb-crawl-and-list/scripts/tests/test-fallback-crawler.mjs +243 -0
  46. package/templates/skills/sb-crawl-and-list/scripts/tests/test-page-classifier.mjs +120 -0
  47. package/templates/skills/sb-crawl-and-list/scripts/tests/test-sitemap-parser.mjs +157 -0
  48. package/templates/skills/sb-extract-assets/SKILL.md +112 -0
  49. package/templates/skills/sb-extract-assets/scripts/extract-assets.mjs +484 -0
  50. package/templates/skills/sb-extract-assets/scripts/tests/test-extract-assets.mjs +112 -0
  51. package/templates/skills/sb-inspect-live/SKILL.md +105 -0
  52. package/templates/skills/sb-inspect-live/scripts/inspect-live.mjs +693 -0
  53. package/templates/skills/sb-inspect-live/scripts/tests/test-inspect-live.mjs +181 -0
  54. package/templates/skills/sb-review-checks/SKILL.md +113 -0
  55. package/templates/skills/sb-review-checks/references/review-rules.md +195 -0
  56. package/templates/skills/sb-review-checks/scripts/lib/anti-patterns.mjs +379 -0
  57. package/templates/skills/sb-review-checks/scripts/lib/cross-reference.mjs +115 -0
  58. package/templates/skills/sb-review-checks/scripts/lib/design-quality.mjs +541 -0
  59. package/templates/skills/sb-review-checks/scripts/review-checks.mjs +250 -0
  60. package/templates/skills/sb-review-checks/scripts/tests/test-anti-patterns.mjs +343 -0
  61. package/templates/skills/sb-review-checks/scripts/tests/test-cross-reference.mjs +170 -0
  62. package/templates/skills/sb-review-checks/scripts/tests/test-design-quality.mjs +493 -0
  63. package/templates/skills/sb-review-checks/scripts/tests/test-review-checks.mjs +267 -0
  64. package/templates/skills/sb-tweak/SKILL.md +130 -0
  65. package/templates/skills/sb-tweak/references/tweak-patterns.md +157 -0
  66. package/templates/skills/sb-tweak/scripts/lib/diff-summarizer.mjs +140 -0
  67. package/templates/skills/sb-tweak/scripts/lib/element-locator.mjs +507 -0
  68. package/templates/skills/sb-tweak/scripts/lib/intent-parser.mjs +324 -0
  69. package/templates/skills/sb-tweak/scripts/tests/test-diff-summarizer.mjs +248 -0
  70. package/templates/skills/sb-tweak/scripts/tests/test-element-locator.mjs +418 -0
  71. package/templates/skills/sb-tweak/scripts/tests/test-intent-parser.mjs +496 -0
  72. package/templates/skills/sb-tweak/scripts/tests/test-tweak.mjs +407 -0
  73. package/templates/skills/sb-tweak/scripts/tweak.mjs +656 -0
  74. package/templates/skills/sb-validate-render/SKILL.md +120 -0
  75. package/templates/skills/sb-validate-render/scripts/tests/test-validate-render.mjs +304 -0
  76. package/templates/skills/sb-validate-render/scripts/validate-render.mjs +645 -0
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env node
2
+ // Tests for lib/sitemap-parser.mjs — pure unit tests, no network.
3
+
4
+ import { strict as assert } from 'node:assert'
5
+ import { parseSitemap, fetchAndParseSitemap } from '../lib/sitemap-parser.mjs'
6
+
7
+ let passed = 0
8
+ let failed = 0
9
+
10
+ async function test(name, fn) {
11
+ try {
12
+ await fn()
13
+ process.stdout.write(`ok - ${name}\n`)
14
+ passed++
15
+ } catch (err) {
16
+ process.stdout.write(`not ok - ${name}\n ${err.message}\n`)
17
+ failed++
18
+ }
19
+ }
20
+
21
+ const URLSET_BASIC = `<?xml version="1.0" encoding="UTF-8"?>
22
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
23
+ <url><loc>https://example.com/</loc></url>
24
+ <url><loc>https://example.com/products/foo</loc></url>
25
+ <url><loc>https://example.com/about</loc></url>
26
+ </urlset>`
27
+
28
+ const URLSET_CDATA = `<?xml version="1.0" encoding="UTF-8"?>
29
+ <urlset>
30
+ <url><loc><![CDATA[https://example.com/path?a=1&b=2]]></loc></url>
31
+ </urlset>`
32
+
33
+ const URLSET_ENTITIES = `<?xml version="1.0" encoding="UTF-8"?>
34
+ <urlset>
35
+ <url><loc>https://example.com/path?a=1&amp;b=2&amp;c=3</loc></url>
36
+ </urlset>`
37
+
38
+ const SITEMAPINDEX = `<?xml version="1.0" encoding="UTF-8"?>
39
+ <sitemapindex>
40
+ <sitemap><loc>https://example.com/sitemap-1.xml</loc></sitemap>
41
+ <sitemap><loc>https://example.com/sitemap-2.xml</loc></sitemap>
42
+ </sitemapindex>`
43
+
44
+ const URLSET_BOM = '<urlset><url><loc>https://example.com/</loc></url></urlset>'
45
+
46
+ const URLSET_FLAT = `<url><loc>https://example.com/a</loc></url><url><loc>https://example.com/b</loc></url>`
47
+
48
+ await test('parseSitemap extracts URLs from a basic urlset', () => {
49
+ const r = parseSitemap(URLSET_BASIC)
50
+ assert.equal(r.kind, 'urlset')
51
+ assert.equal(r.malformed, false)
52
+ assert.deepEqual(r.urlSetUrls, [
53
+ 'https://example.com/',
54
+ 'https://example.com/products/foo',
55
+ 'https://example.com/about',
56
+ ])
57
+ })
58
+
59
+ await test('parseSitemap unwraps CDATA payloads', () => {
60
+ const r = parseSitemap(URLSET_CDATA)
61
+ assert.equal(r.kind, 'urlset')
62
+ assert.deepEqual(r.urlSetUrls, ['https://example.com/path?a=1&b=2'])
63
+ })
64
+
65
+ await test('parseSitemap decodes XML entities in <loc>', () => {
66
+ const r = parseSitemap(URLSET_ENTITIES)
67
+ assert.deepEqual(r.urlSetUrls, ['https://example.com/path?a=1&b=2&c=3'])
68
+ })
69
+
70
+ await test('parseSitemap detects sitemapindex and lists child sitemaps', () => {
71
+ const r = parseSitemap(SITEMAPINDEX)
72
+ assert.equal(r.kind, 'index')
73
+ assert.deepEqual(r.sitemapIndexUrls, [
74
+ 'https://example.com/sitemap-1.xml',
75
+ 'https://example.com/sitemap-2.xml',
76
+ ])
77
+ assert.deepEqual(r.urlSetUrls, [])
78
+ })
79
+
80
+ await test('parseSitemap strips BOM', () => {
81
+ const r = parseSitemap(URLSET_BOM)
82
+ assert.equal(r.kind, 'urlset')
83
+ assert.deepEqual(r.urlSetUrls, ['https://example.com/'])
84
+ })
85
+
86
+ await test('parseSitemap accepts wrapper-less <url><loc> sequences', () => {
87
+ const r = parseSitemap(URLSET_FLAT)
88
+ assert.equal(r.kind, 'urlset')
89
+ assert.equal(r.urlSetUrls.length, 2)
90
+ })
91
+
92
+ await test('parseSitemap returns malformed for empty input', () => {
93
+ const r = parseSitemap('')
94
+ assert.equal(r.malformed, true)
95
+ assert.deepEqual(r.urlSetUrls, [])
96
+ })
97
+
98
+ await test('parseSitemap returns malformed for non-sitemap XML', () => {
99
+ const r = parseSitemap('<html><body>not a sitemap</body></html>')
100
+ assert.equal(r.kind, 'unknown')
101
+ assert.equal(r.malformed, true)
102
+ })
103
+
104
+ await test('fetchAndParseSitemap returns urlset directly', async () => {
105
+ const fetcher = async () => ({ ok: true, status: 200, text: URLSET_BASIC })
106
+ const r = await fetchAndParseSitemap('https://example.com/sitemap.xml', fetcher)
107
+ assert.equal(r.urls.length, 3)
108
+ assert.deepEqual(r.warnings, [])
109
+ })
110
+
111
+ await test('fetchAndParseSitemap recurses into sitemapindex one level', async () => {
112
+ const fetcher = async (url) => {
113
+ if (url.endsWith('/sitemap.xml')) return { ok: true, status: 200, text: SITEMAPINDEX }
114
+ if (url.endsWith('/sitemap-1.xml')) return { ok: true, status: 200, text: URLSET_BASIC }
115
+ if (url.endsWith('/sitemap-2.xml'))
116
+ return {
117
+ ok: true,
118
+ status: 200,
119
+ text: '<urlset><url><loc>https://example.com/extra</loc></url></urlset>',
120
+ }
121
+ return { ok: false, status: 404 }
122
+ }
123
+ const r = await fetchAndParseSitemap('https://example.com/sitemap.xml', fetcher)
124
+ assert.equal(r.urls.length, 4)
125
+ assert.deepEqual(r.warnings, [])
126
+ assert.ok(r.urls.includes('https://example.com/extra'))
127
+ })
128
+
129
+ await test('fetchAndParseSitemap warns on child fetch failure but continues', async () => {
130
+ const fetcher = async (url) => {
131
+ if (url.endsWith('/sitemap.xml')) return { ok: true, status: 200, text: SITEMAPINDEX }
132
+ if (url.endsWith('/sitemap-1.xml')) return { ok: true, status: 200, text: URLSET_BASIC }
133
+ return { ok: false, status: 404 }
134
+ }
135
+ const r = await fetchAndParseSitemap('https://example.com/sitemap.xml', fetcher)
136
+ assert.equal(r.urls.length, 3)
137
+ assert.equal(r.warnings.length, 1)
138
+ assert.match(r.warnings[0], /sitemap-child-failed:.*sitemap-2\.xml/)
139
+ })
140
+
141
+ await test('fetchAndParseSitemap surfaces top-level fetch failure', async () => {
142
+ const fetcher = async () => ({ ok: false, status: 503 })
143
+ const r = await fetchAndParseSitemap('https://example.com/sitemap.xml', fetcher)
144
+ assert.deepEqual(r.urls, [])
145
+ assert.equal(r.warnings.length, 1)
146
+ assert.match(r.warnings[0], /sitemap-fetch-failed:503/)
147
+ })
148
+
149
+ await test('fetchAndParseSitemap surfaces malformed top-level XML', async () => {
150
+ const fetcher = async () => ({ ok: true, status: 200, text: '<html>nope</html>' })
151
+ const r = await fetchAndParseSitemap('https://example.com/sitemap.xml', fetcher)
152
+ assert.deepEqual(r.urls, [])
153
+ assert.deepEqual(r.warnings, ['sitemap-malformed'])
154
+ })
155
+
156
+ process.stdout.write(`\n${passed} passed, ${failed} failed\n`)
157
+ process.exit(failed === 0 ? 0 : 1)
@@ -0,0 +1,112 @@
1
+ ---
2
+ name: sb-extract-assets
3
+ description: Downloads images from inspected URLs, strips identifying metadata (EXIF/XMP/IPTC), renames via content-hash, and dedupes against assets already on disk. Use when the SimilarBuild orchestrator (`/build-page`, `/build-site`, `/clip-section`) requests asset extraction after `sb-inspect-live`, or when the user asks to 'extract assets'.
4
+ ---
5
+
6
+ # sb-extract-assets
7
+
8
+ ## Overview
9
+
10
+ Takes the `imgUrls` list from `sb-inspect-live` and produces a clean, tracking-resistant asset library: each file is downloaded, **all camera/editor metadata stripped**, **renamed to a content-hash** (`sha256(buffer)[:16].<ext>`), and saved into the project's asset folder. Same binary → same hash → automatic dedupe across pages of a site batch and across reruns.
11
+
12
+ The strip is non-negotiable. Cameras, photo editors, CMS plugins, and stock-image platforms inject EXIF/XMP/IPTC blocks that often contain the original photographer's name, GPS coords, software signatures, copyright, and source URLs — all of which leak provenance and make a rebuild trivially traceable to its source. Sharp's default re-encode drops every metadata block while preserving the ICC color profile (so colors render faithfully on calibrated screens). The script never calls `.withMetadata()`.
13
+
14
+ Act as a sanitization gate: anything passed downstream to `sb-build-wp` / `sb-build-shopify` is opaque, deduplicated, and safe to ship. The original filename and source URL live only in the local `assets-map.json` for debug, never in the rendered HTML.
15
+
16
+ ## Inputs
17
+
18
+ | Argument | Required | Default | Notes |
19
+ | -------------------- | -------- | ----------- | -------------------------------------------------------------------------------------- |
20
+ | `inspection-path` | yes\* | — | Path to `inspection.json` from `sb-inspect-live` (script reads `imgUrls`). |
21
+ | `img-urls` | yes\* | — | Inline JSON array `[{url, context, alt}]` — alternative to `--inspection-path`. |
22
+ | `output-dir` | yes | — | Directory to save sanitized assets and `assets-map.json`. |
23
+ | `target` | no | `wp` | `wp` (SVGs returned inline-ready) or `shopify` (SVGs saved as files). |
24
+ | `existing-assets-dir`| no | (none) | Extra directory of already-extracted assets to dedupe against (e.g. sibling pages). |
25
+ | `large-warn-mb` | no | `10` | Warn (stderr) if a single asset is larger than this. |
26
+ | `timeout` | no | `30000` | Per-request fetch timeout (ms). |
27
+
28
+ \*Exactly one of `--inspection-path` or `--img-urls` must be provided.
29
+
30
+ ## Output
31
+
32
+ A single JSON object printed to stdout AND saved to `{output_dir}/assets-map.json`:
33
+
34
+ ```json
35
+ {
36
+ "target": "wp",
37
+ "outputDir": "/abs/path",
38
+ "assets": {
39
+ "https://example.com/cdn/hero@2x.jpg": {
40
+ "localPath": "/abs/path/a3f9b2c14e8d7f01.jpg",
41
+ "hash": "a3f9b2c14e8d7f01",
42
+ "ext": "jpg",
43
+ "bytes": 184320,
44
+ "originalFilename": "hero@2x.jpg",
45
+ "strippedMetadata": true,
46
+ "reusedFrom": null,
47
+ "context": "hero",
48
+ "alt": "Product hero shot"
49
+ },
50
+ "https://example.com/icons/cart.svg": {
51
+ "inline": "<svg ...></svg>",
52
+ "originalFilename": "cart.svg",
53
+ "strippedMetadata": true,
54
+ "context": "header",
55
+ "alt": ""
56
+ }
57
+ },
58
+ "reuseReport": [
59
+ { "url": "https://example.com/cdn/hero-mobile.jpg", "reusedFrom": "https://example.com/cdn/hero@2x.jpg", "reason": "identical-content-hash" }
60
+ ],
61
+ "failed": [
62
+ { "url": "https://example.com/missing.png", "status": 404, "reason": "not-found" }
63
+ ]
64
+ }
65
+ ```
66
+
67
+ `originalFilename` is recorded **only** in this map for debug/audit. The orchestrator must NOT propagate it into HTML output (alt text, filenames, comments) — doing so re-attaches the source identity the strip just removed. SVG inlining for `target=wp` returns sanitized markup directly under `assets[url].inline`; for `target=shopify` SVGs are saved as files like raster images.
68
+
69
+ ## Dependencies
70
+
71
+ The host project must have `sharp` installed (raster strip + content-hash). Node ≥ 20 (uses native `fetch` + `crypto.subtle`). The SimilarBuild installer handles this.
72
+
73
+ ## On Activation
74
+
75
+ 1. **Resolve inputs.** Collect `inspection-path` *or* `img-urls`, plus `output-dir`, `target`, `existing-assets-dir`. Apply defaults above.
76
+
77
+ 2. **Ensure `output_dir` exists.** `mkdir -p` it. If `assets-map.json` already exists in `output_dir`, the script reads it first so reruns are idempotent (same input → same map).
78
+
79
+ 3. **Run the script.** From the project root:
80
+
81
+ ```bash
82
+ node {skill-root}/scripts/extract-assets.mjs \
83
+ --output-dir "{output_dir}" \
84
+ --target "{target}" \
85
+ [--inspection-path "{path}" | --img-urls '<json>'] \
86
+ [--existing-assets-dir "{path}"]
87
+ ```
88
+
89
+ The script handles: HEAD probe + size check, fetch into buffer, sharp re-encode (`.rotate()` first to bake in orientation, then re-encode without metadata, keeping ICC), SVG regex sanitization (`<metadata>`, `<!--…-->`, `inkscape:` / `sodipodi:` namespaces), content-hash naming, dedupe against `output_dir` and `existing-assets-dir`, and the inline-vs-file SVG decision based on `target`. See `scripts/extract-assets.mjs --help` for the full flag list.
90
+
91
+ 4. **Validate the result.** Parse stdout as JSON. If the script exits non-zero, surface stderr to the orchestrator and stop — don't fabricate a partial map.
92
+
93
+ 5. **Forward `failed[]` unchanged.** A 404, network error, or content-type mismatch is **not** fatal for this skill. The orchestrator decides whether to re-prompt the user, fall back to a placeholder, or skip the asset entirely. The script never invents a substitute.
94
+
95
+ 6. **Return the JSON unchanged** to the caller. Do not summarize or strip fields — `sb-build-wp` / `sb-build-shopify` consume `assets[].localPath`, `assets[].inline`, and the `failed[]` list directly.
96
+
97
+ ## Failure modes
98
+
99
+ | Symptom | Likely cause | What to surface |
100
+ | ------------------------------------------------ | --------------------------------------- | ---------------------------------------------------------------- |
101
+ | Script exits non-zero | Missing `sharp`, malformed inspection JSON, write failure | Pass stderr verbatim. Suggest `npm i sharp` if the message mentions sharp. |
102
+ | URL appears in `failed[]` with `status: 404` | Source removed/renamed since inspection | Forward unchanged — orchestrator handles fallback. |
103
+ | URL appears in `failed[]` with `status: 0` | DNS / connection / timeout | Forward unchanged. Orchestrator may retry once. |
104
+ | `strippedMetadata: false` on an asset | Sharp couldn't decode (corrupt or unsupported format) | Asset is saved as-is (raw bytes). Orchestrator should treat it as risky and may opt to drop. |
105
+ | Same URL maps to different hash across runs | Source returned different bytes (CDN A/B, resized) | Treat as authoritative — it's a new asset version. |
106
+
107
+ ## Conventions
108
+
109
+ - Bare paths (e.g. `scripts/extract-assets.mjs`) resolve from the skill root.
110
+ - `{skill-root}` resolves to this skill's installed directory.
111
+ - `{project-root}` resolves to the project working directory.
112
+ - Filenames are always `<16-hex-hash>.<ext>` — never the original. Extension is derived from the **decoded MIME type**, not the source URL (a `.jpg?v=2` querystring on a PNG body is corrected).