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,52 @@
1
+ // Copia templates/* pras paths corretas no projeto destino.
2
+ // install: sobrescreve templates (commands/skills/memory/presets/reports são framework code, versionada via release).
3
+ // update: idem — só preserva o que é state do user (config, per-user memory) e isso fica fora do escopo desta função.
4
+
5
+ import fs from 'node:fs/promises';
6
+ import path from 'node:path';
7
+
8
+ const SKIP_DIRS = new Set(['node_modules', '.git']);
9
+ const SKIP_FILES = new Set(['.DS_Store', 'Thumbs.db']);
10
+
11
+ async function copyDir(src, dest) {
12
+ await fs.mkdir(dest, { recursive: true });
13
+ const entries = await fs.readdir(src, { withFileTypes: true });
14
+ for (const e of entries) {
15
+ if (e.isDirectory() && SKIP_DIRS.has(e.name)) continue;
16
+ if (e.isFile() && SKIP_FILES.has(e.name)) continue;
17
+ const s = path.join(src, e.name);
18
+ const d = path.join(dest, e.name);
19
+ if (e.isDirectory()) await copyDir(s, d);
20
+ else if (e.isFile()) await fs.copyFile(s, d);
21
+ else if (e.isSymbolicLink()) {
22
+ const target = await fs.readlink(s);
23
+ try { await fs.unlink(d); } catch {}
24
+ await fs.symlink(target, d);
25
+ }
26
+ }
27
+ }
28
+
29
+ export async function copyTemplates({ pluginRoot, destRoot }) {
30
+ const tpl = path.join(pluginRoot, 'templates');
31
+ // Sanidade: pluginRoot deve conter templates/
32
+ try { await fs.access(tpl); }
33
+ catch { throw new Error(`templates/ não encontrado em ${pluginRoot} — pacote corrompido?`); }
34
+
35
+ const mappings = [
36
+ { src: path.join(tpl, 'commands'), dest: path.join(destRoot, '.claude', 'commands') },
37
+ { src: path.join(tpl, 'skills'), dest: path.join(destRoot, '.claude', 'skills') },
38
+ { src: path.join(tpl, 'memory'), dest: path.join(destRoot, '.claude', 'skills', 'sb-shared', 'memory') },
39
+ { src: path.join(tpl, 'presets'), dest: path.join(destRoot, '.claude', 'skills', 'sb-shared', 'presets') },
40
+ { src: path.join(tpl, 'reports'), dest: path.join(destRoot, '.claude', 'templates', 'reports') },
41
+ ];
42
+
43
+ for (const { src, dest } of mappings) {
44
+ try { await fs.access(src); }
45
+ catch {
46
+ process.stderr.write(`[similarbuild] aviso: ${path.relative(pluginRoot, src)} não existe — pulando\n`);
47
+ continue;
48
+ }
49
+ process.stderr.write(`[similarbuild] copiando ${path.relative(pluginRoot, src)} → ${path.relative(destRoot, dest)}\n`);
50
+ await copyDir(src, dest);
51
+ }
52
+ }
@@ -0,0 +1,62 @@
1
+ // Instala deps npm no projeto destino + (opcionalmente) baixa Chromium.
2
+ // Spawn herda stdio do parent — user vê o output do npm direto.
3
+
4
+ import { spawn } from 'node:child_process';
5
+ import path from 'node:path';
6
+ import fs from 'node:fs/promises';
7
+
8
+ export const RUNTIME_DEPS = Object.freeze([
9
+ 'playwright',
10
+ 'playwright-extra',
11
+ 'puppeteer-extra-plugin-stealth',
12
+ 'sharp',
13
+ 'pixelmatch',
14
+ 'pngjs',
15
+ 'cheerio',
16
+ 'js-yaml',
17
+ 'liquidjs',
18
+ 'prettier',
19
+ ]);
20
+
21
+ function run(cmd, args, cwd) {
22
+ return new Promise((resolve, reject) => {
23
+ const proc = spawn(cmd, args, { cwd, stdio: 'inherit', shell: process.platform === 'win32' });
24
+ proc.on('error', reject);
25
+ proc.on('exit', (code, signal) => {
26
+ if (code === 0) resolve();
27
+ else reject(new Error(`${cmd} ${args.join(' ')} exited ${code ?? 'signal=' + signal}`));
28
+ });
29
+ });
30
+ }
31
+
32
+ export async function ensurePackageJson(destRoot) {
33
+ const pkgPath = path.join(destRoot, 'package.json');
34
+ try {
35
+ await fs.access(pkgPath);
36
+ return false;
37
+ } catch {
38
+ const slug = (path.basename(destRoot) || 'sb-project')
39
+ .toLowerCase()
40
+ .replace(/[^a-z0-9-]+/g, '-')
41
+ .replace(/^-+|-+$/g, '') || 'sb-project';
42
+ const stub = {
43
+ name: slug,
44
+ version: '0.0.0',
45
+ private: true,
46
+ type: 'module',
47
+ };
48
+ await fs.writeFile(pkgPath, JSON.stringify(stub, null, 2) + '\n');
49
+ process.stderr.write(`[similarbuild] criou package.json stub (name: ${slug})\n`);
50
+ return true;
51
+ }
52
+ }
53
+
54
+ export async function installDeps(destRoot) {
55
+ process.stderr.write(`[similarbuild] npm install --save ${RUNTIME_DEPS.join(' ')}\n`);
56
+ await run('npm', ['install', '--save', ...RUNTIME_DEPS], destRoot);
57
+ }
58
+
59
+ export async function installChromium(destRoot) {
60
+ process.stderr.write(`[similarbuild] npx playwright install chromium\n`);
61
+ await run('npx', ['--yes', 'playwright', 'install', 'chromium'], destRoot);
62
+ }
@@ -0,0 +1,83 @@
1
+ // Lê config interativamente OU retorna defaults com --yes.
2
+ // Usa readline nativo (zero dep) — só 5 perguntas, não vale a pena puxar `prompts`.
3
+
4
+ import readline from 'node:readline/promises';
5
+ import { stdin as input, stdout as output } from 'node:process';
6
+
7
+ export const DEFAULTS = Object.freeze({
8
+ output_folder: './sb-output',
9
+ default_target: 'wp',
10
+ default_viewport: 390,
11
+ auto_correct_max_iterations: 2,
12
+ diff_threshold_percent: 15,
13
+ });
14
+
15
+ const TARGETS = new Set(['wp', 'shopify']);
16
+
17
+ function prefix(msg) {
18
+ return `[similarbuild] ${msg}`;
19
+ }
20
+
21
+ async function askText(rl, prompt, def) {
22
+ const answer = (await rl.question(`${prompt} [${def}]: `)).trim();
23
+ return answer || String(def);
24
+ }
25
+
26
+ async function askInt(rl, prompt, def, { min, max } = {}) {
27
+ while (true) {
28
+ const raw = await askText(rl, prompt, def);
29
+ const n = parseInt(raw, 10);
30
+ if (Number.isNaN(n)) {
31
+ process.stderr.write(prefix(`valor inválido: "${raw}" — esperava número inteiro\n`));
32
+ continue;
33
+ }
34
+ if (typeof min === 'number' && n < min) {
35
+ process.stderr.write(prefix(`valor abaixo do mínimo (${min})\n`));
36
+ continue;
37
+ }
38
+ if (typeof max === 'number' && n > max) {
39
+ process.stderr.write(prefix(`valor acima do máximo (${max})\n`));
40
+ continue;
41
+ }
42
+ return n;
43
+ }
44
+ }
45
+
46
+ async function askChoice(rl, prompt, def, choices) {
47
+ while (true) {
48
+ const raw = (await askText(rl, `${prompt} (${choices.join('/')})`, def)).toLowerCase();
49
+ if (choices.includes(raw)) return raw;
50
+ process.stderr.write(prefix(`valor inválido: "${raw}" — opções: ${choices.join(', ')}\n`));
51
+ }
52
+ }
53
+
54
+ export async function promptConfig({ yes = false } = {}) {
55
+ if (yes) return { ...DEFAULTS };
56
+ const rl = readline.createInterface({ input, output });
57
+ try {
58
+ process.stderr.write(prefix('Configuração inicial — Enter aceita o default.\n'));
59
+ const cfg = {
60
+ output_folder: await askText(rl, 'Pasta de output (relativa ao projeto)', DEFAULTS.output_folder),
61
+ default_target: await askChoice(rl, 'Destino padrão', DEFAULTS.default_target, [...TARGETS]),
62
+ default_viewport: await askInt(rl, 'Viewport mobile-first (px)', DEFAULTS.default_viewport, { min: 240, max: 2048 }),
63
+ auto_correct_max_iterations: await askInt(rl, 'Tentativas de auto-correção antes de escalar', DEFAULTS.auto_correct_max_iterations, { min: 0, max: 10 }),
64
+ diff_threshold_percent: await askInt(rl, 'Threshold de diff% pra "bate com a live"', DEFAULTS.diff_threshold_percent, { min: 0, max: 100 }),
65
+ };
66
+ return cfg;
67
+ } finally {
68
+ rl.close();
69
+ }
70
+ }
71
+
72
+ export async function confirm(question, { defaultYes = true, yes = false } = {}) {
73
+ if (yes) return defaultYes;
74
+ const rl = readline.createInterface({ input, output });
75
+ try {
76
+ const suffix = defaultYes ? '[S/n]' : '[s/N]';
77
+ const answer = (await rl.question(`${question} ${suffix}: `)).trim().toLowerCase();
78
+ if (!answer) return defaultYes;
79
+ return ['s', 'sim', 'y', 'yes'].includes(answer);
80
+ } finally {
81
+ rl.close();
82
+ }
83
+ }
@@ -0,0 +1,19 @@
1
+ // Pattern #16: stderr prefix `[similarbuild]` para todo log do installer.
2
+ const MIN_NODE_MAJOR = 20;
3
+
4
+ export function verifyNode() {
5
+ const v = process.versions.node;
6
+ const major = parseInt(v.split('.')[0], 10);
7
+ if (Number.isNaN(major) || major < MIN_NODE_MAJOR) {
8
+ process.stderr.write(`[similarbuild] Node ${MIN_NODE_MAJOR}+ required. Detected v${v}.\n`);
9
+ process.stderr.write(`[similarbuild] Instale Node 20+: https://nodejs.org/\n`);
10
+ return false;
11
+ }
12
+ return true;
13
+ }
14
+
15
+ export function verifyNpm() {
16
+ // npm vem junto com Node, mas se o user tiver removido, falhamos rápido.
17
+ // Verificação real só roda quando installDeps for chamado (spawn npm).
18
+ return true;
19
+ }
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "similarbuild",
3
+ "version": "0.1.0",
4
+ "description": "Visual migration framework for Claude Code — clone a live page, get a paste-ready WordPress/Elementor or Shopify section file, validated and auto-corrected.",
5
+ "type": "module",
6
+ "bin": {
7
+ "similarbuild": "bin/install.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "lib/",
12
+ "scripts/",
13
+ "templates/",
14
+ "README.md",
15
+ "LICENSE",
16
+ "CHANGELOG.md"
17
+ ],
18
+ "engines": {
19
+ "node": ">=20"
20
+ },
21
+ "scripts": {
22
+ "sync-templates": "node scripts/sync-templates.mjs",
23
+ "prepublishOnly": "node scripts/sync-templates.mjs",
24
+ "smoke:install": "node bin/install.js --help",
25
+ "test": "echo \"no top-level test suite — see .claude/skills/sb-*/scripts/tests/ for per-skill tests\""
26
+ },
27
+ "keywords": [
28
+ "claude-code",
29
+ "claude",
30
+ "anthropic",
31
+ "wordpress",
32
+ "elementor",
33
+ "shopify",
34
+ "liquid",
35
+ "visual-migration",
36
+ "page-clone",
37
+ "playwright",
38
+ "scaffold",
39
+ "installer"
40
+ ],
41
+ "author": "SimilarBuild Authors <noreply@github.com>",
42
+ "license": "MIT",
43
+ "homepage": "https://github.com/eggonbassoli/similarbuild#readme",
44
+ "bugs": {
45
+ "url": "https://github.com/eggonbassoli/similarbuild/issues"
46
+ },
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "git+https://github.com/eggonbassoli/similarbuild.git"
50
+ },
51
+ "devDependencies": {
52
+ "cheerio": "^1.2.0",
53
+ "js-yaml": "^4.1.1",
54
+ "liquidjs": "^10.25.7",
55
+ "pixelmatch": "^7.2.0",
56
+ "playwright": "^1.59.1",
57
+ "playwright-extra": "^4.3.6",
58
+ "pngjs": "^7.0.0",
59
+ "prettier": "^3.8.3",
60
+ "puppeteer-extra-plugin-stealth": "^2.11.2",
61
+ "sharp": "^0.34.5"
62
+ }
63
+ }
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+ // Sync .claude/{commands,skills/sb-*} → templates/{commands,skills/}
3
+ // Roda antes de `npm publish` (prepublishOnly) e como conveniência manual durante dev.
4
+ // Mantém `templates/` como source-of-truth pro tarball; `.claude/` é o working dir.
5
+
6
+ import fs from 'node:fs/promises';
7
+ import path from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
+ const ROOT = path.resolve(__dirname, '..');
12
+
13
+ const SKIP_DIRS = new Set(['node_modules', '.git', '.DS_Store']);
14
+ const SKIP_FILES = new Set(['.DS_Store', 'Thumbs.db']);
15
+
16
+ async function rmrf(p) {
17
+ await fs.rm(p, { recursive: true, force: true });
18
+ }
19
+
20
+ async function copyDir(src, dest) {
21
+ await fs.mkdir(dest, { recursive: true });
22
+ const entries = await fs.readdir(src, { withFileTypes: true });
23
+ for (const e of entries) {
24
+ if (e.isDirectory() && SKIP_DIRS.has(e.name)) continue;
25
+ if (e.isFile() && SKIP_FILES.has(e.name)) continue;
26
+ const s = path.join(src, e.name);
27
+ const d = path.join(dest, e.name);
28
+ if (e.isDirectory()) await copyDir(s, d);
29
+ else if (e.isFile()) await fs.copyFile(s, d);
30
+ else if (e.isSymbolicLink()) {
31
+ const target = await fs.readlink(s);
32
+ await fs.symlink(target, d);
33
+ }
34
+ }
35
+ }
36
+
37
+ async function syncCommands() {
38
+ const src = path.join(ROOT, '.claude', 'commands');
39
+ const dest = path.join(ROOT, 'templates', 'commands');
40
+ process.stderr.write(`[sync-templates] commands: ${path.relative(ROOT, src)} → ${path.relative(ROOT, dest)}\n`);
41
+ await rmrf(dest);
42
+ await copyDir(src, dest);
43
+ }
44
+
45
+ async function syncSkills() {
46
+ const src = path.join(ROOT, '.claude', 'skills');
47
+ const dest = path.join(ROOT, 'templates', 'skills');
48
+ process.stderr.write(`[sync-templates] skills: ${path.relative(ROOT, src)}/sb-* → ${path.relative(ROOT, dest)}/\n`);
49
+ await rmrf(dest);
50
+ await fs.mkdir(dest, { recursive: true });
51
+ const entries = await fs.readdir(src, { withFileTypes: true });
52
+ let count = 0;
53
+ for (const e of entries) {
54
+ if (!e.isDirectory()) continue;
55
+ if (!e.name.startsWith('sb-')) continue;
56
+ await copyDir(path.join(src, e.name), path.join(dest, e.name));
57
+ count++;
58
+ }
59
+ process.stderr.write(`[sync-templates] sincronizou ${count} skills sb-*\n`);
60
+ }
61
+
62
+ async function main() {
63
+ await syncCommands();
64
+ await syncSkills();
65
+ process.stderr.write('[sync-templates] ok\n');
66
+ }
67
+
68
+ main().catch((e) => {
69
+ process.stderr.write(`[sync-templates] erro: ${e.stack || e.message}\n`);
70
+ process.exit(1);
71
+ });