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,350 @@
1
+ #!/usr/bin/env node
2
+ // test-compare-tokens.mjs — Full coverage of the token comparison logic.
3
+ // Pure functions, no deps — runs anywhere Node ≥ 20 runs.
4
+
5
+ import { fileURLToPath } from 'node:url'
6
+ import { dirname, resolve } from 'node:path'
7
+ import { strict as assert } from 'node:assert'
8
+
9
+ const here = dirname(fileURLToPath(import.meta.url))
10
+ const LIB = resolve(here, '..', 'lib', 'compare-tokens.mjs')
11
+
12
+ const { compareTokens, compareGeometryBlocks, sortBySeverity, parsePx, parseLineHeight, normalizeColor } = await import(LIB)
13
+
14
+ let passed = 0
15
+ let failed = 0
16
+
17
+ function test(name, fn) {
18
+ try {
19
+ fn()
20
+ process.stdout.write(`ok - ${name}\n`)
21
+ passed++
22
+ } catch (err) {
23
+ process.stdout.write(`not ok - ${name}\n ${err.message}\n`)
24
+ failed++
25
+ }
26
+ }
27
+
28
+ // ─── parsePx ────────────────────────────────────────────────────────────────
29
+
30
+ test('parsePx: handles px', () => {
31
+ assert.equal(parsePx('24px'), 24)
32
+ assert.equal(parsePx('0px'), 0)
33
+ assert.equal(parsePx('-4px'), -4)
34
+ })
35
+
36
+ test('parsePx: handles rem (16px base)', () => {
37
+ assert.equal(parsePx('1.5rem'), 24)
38
+ assert.equal(parsePx('2rem'), 32)
39
+ })
40
+
41
+ test('parsePx: handles em (16px base)', () => {
42
+ assert.equal(parsePx('1em'), 16)
43
+ })
44
+
45
+ test('parsePx: returns null on auto/none/normal/empty', () => {
46
+ assert.equal(parsePx('auto'), null)
47
+ assert.equal(parsePx('none'), null)
48
+ assert.equal(parsePx('normal'), null)
49
+ assert.equal(parsePx(''), null)
50
+ assert.equal(parsePx(null), null)
51
+ assert.equal(parsePx(undefined), null)
52
+ })
53
+
54
+ test('parsePx: returns null on garbage', () => {
55
+ assert.equal(parsePx('foo'), null)
56
+ assert.equal(parsePx('24'), 24) // bare number defaults to px
57
+ })
58
+
59
+ // ─── parseLineHeight ────────────────────────────────────────────────────────
60
+
61
+ test('parseLineHeight: unitless', () => {
62
+ const r = parseLineHeight('1.5')
63
+ assert.equal(r.kind, 'unitless')
64
+ assert.equal(r.value, 1.5)
65
+ })
66
+
67
+ test('parseLineHeight: px', () => {
68
+ const r = parseLineHeight('24px')
69
+ assert.equal(r.kind, 'px')
70
+ assert.equal(r.value, 24)
71
+ })
72
+
73
+ test('parseLineHeight: normal', () => {
74
+ const r = parseLineHeight('normal')
75
+ assert.equal(r.kind, 'normal')
76
+ assert.equal(r.value, null)
77
+ })
78
+
79
+ // ─── normalizeColor ─────────────────────────────────────────────────────────
80
+
81
+ test('normalizeColor: hex 6-digit', () => {
82
+ assert.equal(normalizeColor('#d8112a'), '#d8112a')
83
+ assert.equal(normalizeColor('#D8112A'), '#d8112a')
84
+ })
85
+
86
+ test('normalizeColor: hex 3-digit expands', () => {
87
+ assert.equal(normalizeColor('#abc'), '#aabbcc')
88
+ })
89
+
90
+ test('normalizeColor: hex 8-digit drops alpha', () => {
91
+ assert.equal(normalizeColor('#d8112aff'), '#d8112a')
92
+ })
93
+
94
+ test('normalizeColor: rgb()', () => {
95
+ assert.equal(normalizeColor('rgb(216, 17, 42)'), '#d8112a')
96
+ })
97
+
98
+ test('normalizeColor: rgba() drops alpha', () => {
99
+ assert.equal(normalizeColor('rgba(216, 17, 42, 0.5)'), '#d8112a')
100
+ })
101
+
102
+ test('normalizeColor: rgb() space-separated with alpha slash', () => {
103
+ assert.equal(normalizeColor('rgb(216 17 42 / 0.5)'), '#d8112a')
104
+ })
105
+
106
+ test('normalizeColor: returns null on transparent/inherit/currentcolor', () => {
107
+ assert.equal(normalizeColor('transparent'), null)
108
+ assert.equal(normalizeColor('inherit'), null)
109
+ assert.equal(normalizeColor('currentcolor'), null)
110
+ })
111
+
112
+ // ─── sortBySeverity ─────────────────────────────────────────────────────────
113
+
114
+ test('sortBySeverity: high before medium before low (stable)', () => {
115
+ const sorted = sortBySeverity([
116
+ { area: 'a', severity: 'low' },
117
+ { area: 'b', severity: 'high' },
118
+ { area: 'c', severity: 'medium' },
119
+ { area: 'd', severity: 'high' },
120
+ { area: 'e', severity: 'low' },
121
+ ])
122
+ assert.deepEqual(
123
+ sorted.map((d) => d.area),
124
+ ['b', 'd', 'c', 'a', 'e'],
125
+ )
126
+ })
127
+
128
+ // ─── compareTokens — typography ─────────────────────────────────────────────
129
+
130
+ test('compareTokens: identical tokens yield no diffs', () => {
131
+ const tokens = {
132
+ h1: { present: true, 'font-size': '24px', 'font-weight': '300', color: '#000', 'font-family': 'Roboto', 'line-height': '1.2' },
133
+ h2: { present: false },
134
+ h3: { present: false },
135
+ body: { 'font-size': '18px', 'font-weight': '400', color: '#111', 'font-family': 'Roboto' },
136
+ button: { present: false },
137
+ container: { computedRenderedW: 390 },
138
+ images: { count: 3 },
139
+ }
140
+ const diffs = compareTokens(tokens, JSON.parse(JSON.stringify(tokens)))
141
+ assert.deepEqual(diffs, [])
142
+ })
143
+
144
+ test('compareTokens: h1 font-size 1px diff is within tolerance', () => {
145
+ const live = { h1: { present: true, 'font-size': '24px' } }
146
+ const build = { h1: { present: true, 'font-size': '25px' } }
147
+ const diffs = compareTokens(live, build)
148
+ assert.equal(diffs.length, 0)
149
+ })
150
+
151
+ test('compareTokens: h1 font-size 3px diff flagged high', () => {
152
+ const live = { h1: { present: true, 'font-size': '27px' } }
153
+ const build = { h1: { present: true, 'font-size': '24px' } }
154
+ const diffs = compareTokens(live, build)
155
+ assert.equal(diffs.length, 1)
156
+ assert.equal(diffs[0].area, 'h1')
157
+ assert.equal(diffs[0].severity, 'high')
158
+ assert.match(diffs[0].issue, /24px vs 27px expected/)
159
+ assert.equal(diffs[0].deltaPx, -3)
160
+ })
161
+
162
+ test('compareTokens: h3 font-size diff flagged medium (not high)', () => {
163
+ const live = { h3: { present: true, 'font-size': '20px' } }
164
+ const build = { h3: { present: true, 'font-size': '24px' } }
165
+ const diffs = compareTokens(live, build)
166
+ assert.equal(diffs.length, 1)
167
+ assert.equal(diffs[0].severity, 'medium')
168
+ })
169
+
170
+ test('compareTokens: h1 color hex diff flagged high', () => {
171
+ const live = { h1: { present: true, color: '#d8112a' } }
172
+ const build = { h1: { present: true, color: '#d82a11' } }
173
+ const diffs = compareTokens(live, build)
174
+ assert.equal(diffs.length, 1)
175
+ assert.equal(diffs[0].severity, 'high')
176
+ assert.match(diffs[0].issue, /color #d82a11 vs #d8112a expected/)
177
+ })
178
+
179
+ test('compareTokens: h1 color rgb() vs hex equivalent → no diff', () => {
180
+ const live = { h1: { present: true, color: '#d8112a' } }
181
+ const build = { h1: { present: true, color: 'rgb(216, 17, 42)' } }
182
+ const diffs = compareTokens(live, build)
183
+ assert.equal(diffs.length, 0)
184
+ })
185
+
186
+ test('compareTokens: h1 font-weight diff flagged medium', () => {
187
+ const live = { h1: { present: true, 'font-weight': '300' } }
188
+ const build = { h1: { present: true, 'font-weight': '700' } }
189
+ const diffs = compareTokens(live, build)
190
+ assert.equal(diffs.length, 1)
191
+ assert.equal(diffs[0].severity, 'medium')
192
+ assert.match(diffs[0].issue, /font-weight/)
193
+ })
194
+
195
+ test('compareTokens: h1 font-family head diff flagged low', () => {
196
+ const live = { h1: { present: true, 'font-family': 'Roboto, sans-serif' } }
197
+ const build = { h1: { present: true, 'font-family': 'Inter, sans-serif' } }
198
+ const diffs = compareTokens(live, build)
199
+ assert.equal(diffs.length, 1)
200
+ assert.equal(diffs[0].severity, 'low')
201
+ })
202
+
203
+ test('compareTokens: h1 font-family same head with different fallbacks → no diff', () => {
204
+ const live = { h1: { present: true, 'font-family': 'Roboto, sans-serif' } }
205
+ const build = { h1: { present: true, 'font-family': 'Roboto, Arial, sans-serif' } }
206
+ const diffs = compareTokens(live, build)
207
+ assert.equal(diffs.length, 0)
208
+ })
209
+
210
+ test('compareTokens: h1 missing in build flagged high', () => {
211
+ const live = { h1: { present: true, text: 'Hero' } }
212
+ const build = { h1: { present: false } }
213
+ const diffs = compareTokens(live, build)
214
+ assert.equal(diffs.length, 1)
215
+ assert.equal(diffs[0].severity, 'high')
216
+ assert.equal(diffs[0].liveText, 'Hero')
217
+ })
218
+
219
+ test('compareTokens: h1 missing in live but present in build flagged medium', () => {
220
+ const live = { h1: { present: false } }
221
+ const build = { h1: { present: true } }
222
+ const diffs = compareTokens(live, build)
223
+ assert.equal(diffs.length, 1)
224
+ assert.equal(diffs[0].severity, 'medium')
225
+ })
226
+
227
+ // ─── compareTokens — button ─────────────────────────────────────────────────
228
+
229
+ test('compareTokens: button height 2px diff within tolerance', () => {
230
+ const live = { button: { present: true, renderedH: 40 } }
231
+ const build = { button: { present: true, renderedH: 42 } }
232
+ const diffs = compareTokens(live, build)
233
+ assert.equal(diffs.length, 0)
234
+ })
235
+
236
+ test('compareTokens: button height 4px diff flagged medium', () => {
237
+ const live = { button: { present: true, renderedH: 40 } }
238
+ const build = { button: { present: true, renderedH: 44 } }
239
+ const diffs = compareTokens(live, build)
240
+ assert.equal(diffs.length, 1)
241
+ assert.equal(diffs[0].severity, 'medium')
242
+ assert.equal(diffs[0].deltaPx, 4)
243
+ })
244
+
245
+ test('compareTokens: button background-color diff flagged high', () => {
246
+ const live = { button: { present: true, 'background-color': '#000' } }
247
+ const build = { button: { present: true, 'background-color': '#fff' } }
248
+ const diffs = compareTokens(live, build)
249
+ assert.equal(diffs.length, 1)
250
+ assert.equal(diffs[0].severity, 'high')
251
+ assert.match(diffs[0].issue, /background-color/)
252
+ })
253
+
254
+ // ─── compareTokens — container & images ─────────────────────────────────────
255
+
256
+ test('compareTokens: container width 5px diff within tolerance', () => {
257
+ const live = { container: { computedRenderedW: 390 } }
258
+ const build = { container: { computedRenderedW: 395 } }
259
+ const diffs = compareTokens(live, build)
260
+ assert.equal(diffs.length, 0)
261
+ })
262
+
263
+ test('compareTokens: container width 10px diff flagged low', () => {
264
+ const live = { container: { computedRenderedW: 390 } }
265
+ const build = { container: { computedRenderedW: 400 } }
266
+ const diffs = compareTokens(live, build)
267
+ assert.equal(diffs.length, 1)
268
+ assert.equal(diffs[0].severity, 'low')
269
+ })
270
+
271
+ test('compareTokens: image count drop flagged high', () => {
272
+ const live = { images: { count: 5 } }
273
+ const build = { images: { count: 3 } }
274
+ const diffs = compareTokens(live, build)
275
+ assert.equal(diffs.length, 1)
276
+ assert.equal(diffs[0].severity, 'high')
277
+ })
278
+
279
+ test('compareTokens: image count rise flagged medium', () => {
280
+ const live = { images: { count: 3 } }
281
+ const build = { images: { count: 5 } }
282
+ const diffs = compareTokens(live, build)
283
+ assert.equal(diffs.length, 1)
284
+ assert.equal(diffs[0].severity, 'medium')
285
+ })
286
+
287
+ // ─── priority sort end-to-end ───────────────────────────────────────────────
288
+
289
+ test('compareTokens: many diffs → sorted high → medium → low', () => {
290
+ const live = {
291
+ h1: { present: true, 'font-size': '27px', color: '#d8112a', 'font-family': 'Roboto, sans-serif' },
292
+ button: { present: true, renderedH: 40 },
293
+ container: { computedRenderedW: 390 },
294
+ }
295
+ const build = {
296
+ h1: { present: true, 'font-size': '24px', color: '#d82a11', 'font-family': 'Inter, sans-serif' },
297
+ button: { present: true, renderedH: 50 },
298
+ container: { computedRenderedW: 410 },
299
+ }
300
+ const diffs = compareTokens(live, build)
301
+ // Severities, in order, must descend
302
+ const sevs = diffs.map((d) => d.severity)
303
+ for (let i = 1; i < sevs.length; i++) {
304
+ const order = { high: 0, medium: 1, low: 2 }
305
+ assert.ok(order[sevs[i]] >= order[sevs[i - 1]], `order broken at ${i}: ${sevs.join(',')}`)
306
+ }
307
+ // We expect at least one high, one medium, one low
308
+ assert.ok(sevs.includes('high'))
309
+ assert.ok(sevs.includes('medium'))
310
+ assert.ok(sevs.includes('low'))
311
+ })
312
+
313
+ // ─── compareGeometryBlocks ──────────────────────────────────────────────────
314
+
315
+ test('compareGeometryBlocks: build overflow when live does not → high', () => {
316
+ const live = { viewportOverflow: false, totalHeight: 1000 }
317
+ const build = { viewportOverflow: true, totalHeight: 1000 }
318
+ const diffs = compareGeometryBlocks(live, build)
319
+ assert.equal(diffs.length, 1)
320
+ assert.equal(diffs[0].severity, 'high')
321
+ assert.match(diffs[0].issue, /overflows/)
322
+ })
323
+
324
+ test('compareGeometryBlocks: 50% taller build flagged high', () => {
325
+ const live = { viewportOverflow: false, totalHeight: 1000 }
326
+ const build = { viewportOverflow: false, totalHeight: 1500 }
327
+ const diffs = compareGeometryBlocks(live, build)
328
+ assert.equal(diffs.length, 1)
329
+ assert.equal(diffs[0].severity, 'high')
330
+ })
331
+
332
+ test('compareGeometryBlocks: 10% taller build within tolerance', () => {
333
+ const live = { viewportOverflow: false, totalHeight: 1000 }
334
+ const build = { viewportOverflow: false, totalHeight: 1100 }
335
+ const diffs = compareGeometryBlocks(live, build)
336
+ assert.equal(diffs.length, 0)
337
+ })
338
+
339
+ test('compareTokens: null inputs return empty diffs', () => {
340
+ assert.deepEqual(compareTokens(null, null), [])
341
+ assert.deepEqual(compareTokens(null, {}), [])
342
+ assert.deepEqual(compareTokens({}, null), [])
343
+ })
344
+
345
+ if (failed > 0) {
346
+ process.stdout.write(`\n${failed} failed, ${passed} passed\n`)
347
+ process.exit(1)
348
+ }
349
+ process.stdout.write(`\n${passed} passed\n`)
350
+ process.exit(0)