rebly-sections 1.0.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/README.md +67 -0
- package/assets/SKILL.md +65 -0
- package/assets/commands/section.md +27 -0
- package/assets/commands/theme-init.md +20 -0
- package/assets/data/component-library/_index.csv +19 -0
- package/assets/data/component-library/announcement-bar.liquid +69 -0
- package/assets/data/component-library/blog-posts-grid.liquid +77 -0
- package/assets/data/component-library/collection-list.liquid +71 -0
- package/assets/data/component-library/cta-banner.liquid +58 -0
- package/assets/data/component-library/faq-accordion.liquid +76 -0
- package/assets/data/component-library/features-grid.liquid +94 -0
- package/assets/data/component-library/hero-banner.liquid +104 -0
- package/assets/data/component-library/image-gallery.liquid +77 -0
- package/assets/data/component-library/image-with-text.liquid +84 -0
- package/assets/data/component-library/logo-cloud.liquid +70 -0
- package/assets/data/component-library/newsletter-signup.liquid +72 -0
- package/assets/data/component-library/pricing-table.liquid +101 -0
- package/assets/data/component-library/product-grid.liquid +74 -0
- package/assets/data/component-library/rich-text-content.liquid +52 -0
- package/assets/data/component-library/stats-counter.liquid +73 -0
- package/assets/data/component-library/team-members.liquid +81 -0
- package/assets/data/component-library/testimonials-slider.liquid +93 -0
- package/assets/data/component-library/video-hero.liquid +82 -0
- package/assets/data/data-version.json +19 -0
- package/assets/data/design-tokens.csv +57 -0
- package/assets/data/liquid-stdlib.csv +134 -0
- package/assets/data/schema-library.csv +46 -0
- package/assets/data/shopify-best-practices.csv +36 -0
- package/assets/data/theme-dna.csv +20 -0
- package/assets/scripts/core.py +194 -0
- package/assets/scripts/quality-gate.py +179 -0
- package/assets/scripts/search.py +72 -0
- package/assets/scripts/section-generator.py +202 -0
- package/assets/scripts/theme-init.py +181 -0
- package/assets/templates/generation-prompt.md +31 -0
- package/assets/templates/section-base.liquid +49 -0
- package/assets/templates/theme-profile-template.md +21 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +70 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +14 -0
- package/dist/utils/copy-assets.d.ts +1 -0
- package/dist/utils/copy-assets.js +44 -0
- package/dist/utils/detect-platform.d.ts +6 -0
- package/dist/utils/detect-platform.js +18 -0
- package/package.json +35 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Rebly Sections Generator - Assemble context for Claude to generate Shopify sections
|
|
5
|
+
Usage: python3 section-generator.py "name" "description" [--theme-profile path] [--refine path]
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import sys
|
|
10
|
+
import io
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
# Add scripts dir to path for sibling imports
|
|
14
|
+
_scripts_dir = str(Path(__file__).parent)
|
|
15
|
+
if _scripts_dir not in sys.path:
|
|
16
|
+
sys.path.append(_scripts_dir)
|
|
17
|
+
from core import search, search_component, DATA_DIR
|
|
18
|
+
|
|
19
|
+
TEMPLATE_DIR = Path(__file__).parent.parent / "templates"
|
|
20
|
+
|
|
21
|
+
# UTF-8 stdout
|
|
22
|
+
if sys.stdout.encoding and sys.stdout.encoding.lower() != 'utf-8':
|
|
23
|
+
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def load_theme_profile(path):
|
|
27
|
+
"""Load theme-profile.md or return defaults"""
|
|
28
|
+
if path and Path(path).exists():
|
|
29
|
+
return Path(path).read_text(encoding="utf-8")
|
|
30
|
+
return "No theme profile available. Use generic OS2.0 conventions:\n- CSS Variable Prefix: --\n- Section Naming: kebab-case\n- Follow standard OS2.0 patterns"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def load_template(name):
|
|
34
|
+
"""Load a template file"""
|
|
35
|
+
path = TEMPLATE_DIR / name
|
|
36
|
+
if path.exists():
|
|
37
|
+
return path.read_text(encoding="utf-8")
|
|
38
|
+
return ""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def search_components(description, max_results=2):
|
|
42
|
+
"""Search component library for closest pattern"""
|
|
43
|
+
result = search_component(description, max_results=max_results)
|
|
44
|
+
if "error" in result or not result.get("results"):
|
|
45
|
+
return "No matching component patterns found."
|
|
46
|
+
|
|
47
|
+
output = []
|
|
48
|
+
for r in result["results"]:
|
|
49
|
+
output.append(f"**{r.get('Name', 'Unknown')}** ({r.get('Category', '')}, {r.get('Difficulty', '')})")
|
|
50
|
+
output.append(f"Blocks: {r.get('Blocks Used', '')} | Settings: {r.get('Settings Count', '')}")
|
|
51
|
+
if 'liquid_content' in r:
|
|
52
|
+
output.append(f"```liquid\n{r['liquid_content'][:2000]}\n```")
|
|
53
|
+
output.append("")
|
|
54
|
+
return "\n".join(output)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def search_schema_settings(description, max_results=5):
|
|
58
|
+
"""Search schema-library for relevant setting types"""
|
|
59
|
+
result = search(description, domain="schema", max_results=max_results)
|
|
60
|
+
if "error" in result or not result.get("results"):
|
|
61
|
+
return "No matching schema settings found."
|
|
62
|
+
|
|
63
|
+
output = []
|
|
64
|
+
for r in result["results"]:
|
|
65
|
+
output.append(f"- **{r.get('Type', '')}** ({r.get('Category', '')}): {r.get('Returns', '')} — Props: {r.get('Required Props', '')}")
|
|
66
|
+
json_ex = r.get('JSON Example', '')
|
|
67
|
+
if json_ex:
|
|
68
|
+
output.append(f" ```json\n {json_ex[:300]}\n ```")
|
|
69
|
+
return "\n".join(output)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def search_best_practices(description, max_results=5):
|
|
73
|
+
"""Search best practices for applicable rules"""
|
|
74
|
+
result = search(description, domain="practices", max_results=max_results)
|
|
75
|
+
if "error" in result or not result.get("results"):
|
|
76
|
+
return "No matching best practices found."
|
|
77
|
+
|
|
78
|
+
output = []
|
|
79
|
+
for r in result["results"]:
|
|
80
|
+
output.append(f"- **[{r.get('Severity', '')}] {r.get('Rule', '')}**: {r.get('Description', '')}")
|
|
81
|
+
do = r.get('Do', '')
|
|
82
|
+
dont = r.get('Dont', '')
|
|
83
|
+
if do:
|
|
84
|
+
output.append(f" - DO: {do}")
|
|
85
|
+
if dont:
|
|
86
|
+
output.append(f" - DON'T: {dont}")
|
|
87
|
+
return "\n".join(output)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def try_ui_ux_bridge(ui_style_keywords):
|
|
91
|
+
"""Optional: query ui-ux-pro-max if available"""
|
|
92
|
+
try:
|
|
93
|
+
ux_search_path = Path(__file__).parent.parent.parent.parent / ".claude" / "skills" / "ui-ux-pro-max" / "scripts" / "core.py"
|
|
94
|
+
if not ux_search_path.exists():
|
|
95
|
+
ux_search_path = Path(__file__).parent.parent.parent.parent / ".research" / "ui-ux-pro-max-skill" / "src" / "ui-ux-pro-max" / "scripts" / "core.py"
|
|
96
|
+
|
|
97
|
+
if ux_search_path.exists():
|
|
98
|
+
import importlib.util
|
|
99
|
+
spec = importlib.util.spec_from_file_location("ux_core", ux_search_path)
|
|
100
|
+
ux_module = importlib.util.module_from_spec(spec)
|
|
101
|
+
spec.loader.exec_module(ux_module)
|
|
102
|
+
result = ux_module.search(ui_style_keywords, domain="style", max_results=2)
|
|
103
|
+
if result.get("results"):
|
|
104
|
+
lines = ["**UI/UX Style Recommendations (from ui-ux-pro-max):**"]
|
|
105
|
+
for r in result["results"]:
|
|
106
|
+
lines.append(f"- {r.get('Style Category', '')}: {r.get('Keywords', '')}")
|
|
107
|
+
lines.append(f" Effects: {r.get('Effects & Animation', '')}")
|
|
108
|
+
return "\n".join(lines)
|
|
109
|
+
except Exception:
|
|
110
|
+
pass
|
|
111
|
+
return "N/A — ui-ux-pro-max not available"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def assemble_context(name, description, theme_profile, component, schema, practices, design_recs):
|
|
115
|
+
"""Merge all context into generation prompt"""
|
|
116
|
+
template = load_template("generation-prompt.md")
|
|
117
|
+
base_template = load_template("section-base.liquid")
|
|
118
|
+
|
|
119
|
+
if template:
|
|
120
|
+
content = template
|
|
121
|
+
content = content.replace("{{ name }}", name)
|
|
122
|
+
content = content.replace("{{ description }}", description)
|
|
123
|
+
content = content.replace("{{ theme_profile_summary }}", theme_profile)
|
|
124
|
+
content = content.replace("{{ component_pattern_content }}", component)
|
|
125
|
+
content = content.replace("{{ schema_search_results }}", schema)
|
|
126
|
+
content = content.replace("{{ best_practices_results }}", practices)
|
|
127
|
+
content = content.replace("{{ ui_ux_recommendations }}", design_recs)
|
|
128
|
+
base_block = f"```liquid\n{base_template}\n```" if base_template else "See standard OS2.0 section structure"
|
|
129
|
+
content = content.replace("{{ section_base_content }}", base_block)
|
|
130
|
+
return content
|
|
131
|
+
|
|
132
|
+
# Fallback: manual assembly
|
|
133
|
+
return f"""# Generate Shopify Section: {name}
|
|
134
|
+
|
|
135
|
+
## Description
|
|
136
|
+
{description}
|
|
137
|
+
|
|
138
|
+
## Theme Context
|
|
139
|
+
{theme_profile}
|
|
140
|
+
|
|
141
|
+
## Closest Pattern Template
|
|
142
|
+
{component}
|
|
143
|
+
|
|
144
|
+
## Relevant Schema Settings
|
|
145
|
+
{schema}
|
|
146
|
+
|
|
147
|
+
## Applicable Best Practices
|
|
148
|
+
{practices}
|
|
149
|
+
|
|
150
|
+
## Design Recommendations
|
|
151
|
+
{design_recs}
|
|
152
|
+
|
|
153
|
+
## Base Template
|
|
154
|
+
```liquid
|
|
155
|
+
{base_template}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Requirements
|
|
159
|
+
- OS2.0 compliant (presets, @app blocks, block.shopify_attributes)
|
|
160
|
+
- Scoped CSS via #section-{{{{ section.id }}}}
|
|
161
|
+
- Responsive (mobile-first)
|
|
162
|
+
- Accessible (semantic HTML, aria-labels, alt text)
|
|
163
|
+
- Performance (lazy-load non-hero images, no Liquid in CSS/JS)
|
|
164
|
+
- Use {{% render %}} not {{% include %}}
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def main():
|
|
169
|
+
parser = argparse.ArgumentParser(description="Rebly Sections Generator")
|
|
170
|
+
parser.add_argument("name", help="Section name (e.g., 'hero', 'pricing-table')")
|
|
171
|
+
parser.add_argument("description", help="Section description")
|
|
172
|
+
parser.add_argument("--theme-profile", "-t", default=None, help="Path to theme-profile.md")
|
|
173
|
+
parser.add_argument("--refine", "-r", default=None, help="Path to existing section.liquid to refine")
|
|
174
|
+
args = parser.parse_args()
|
|
175
|
+
|
|
176
|
+
theme = load_theme_profile(args.theme_profile)
|
|
177
|
+
component = search_components(f"{args.name} {args.description}")
|
|
178
|
+
schema = search_schema_settings(args.description)
|
|
179
|
+
practices = search_best_practices(args.description)
|
|
180
|
+
design_recs = try_ui_ux_bridge(args.name + " " + args.description)
|
|
181
|
+
|
|
182
|
+
if args.refine:
|
|
183
|
+
refine_path = Path(args.refine)
|
|
184
|
+
try:
|
|
185
|
+
existing = refine_path.read_text(encoding="utf-8") if refine_path.exists() else None
|
|
186
|
+
except OSError:
|
|
187
|
+
existing = None
|
|
188
|
+
if existing:
|
|
189
|
+
try:
|
|
190
|
+
from quality_gate import run_quality_gate
|
|
191
|
+
gate_result = run_quality_gate(args.refine)
|
|
192
|
+
refine_context = f"\n## Existing Section (for refinement)\n```liquid\n{existing[:3000]}\n```\n\n## Quality Gate Results\n{gate_result}\n\nPlease fix the issues identified above while preserving the section's functionality."
|
|
193
|
+
component += refine_context
|
|
194
|
+
except ImportError:
|
|
195
|
+
component += f"\n## Existing Section (for refinement)\n```liquid\n{existing[:3000]}\n```"
|
|
196
|
+
|
|
197
|
+
context = assemble_context(args.name, args.description, theme, component, schema, practices, design_recs)
|
|
198
|
+
print(context)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
if __name__ == "__main__":
|
|
202
|
+
main()
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Rebly Sections Theme Init - Scan Shopify theme and generate profile.
|
|
5
|
+
Usage: python3 theme-init.py /path/to/theme [--output theme-profile.md]
|
|
6
|
+
Stdlib only. No external dependencies.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import csv
|
|
11
|
+
import json
|
|
12
|
+
import re
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from datetime import date
|
|
16
|
+
|
|
17
|
+
DATA_DIR = Path(__file__).parent.parent / "data"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def detect_theme_info(theme_path: Path) -> dict:
|
|
21
|
+
"""Read config/settings_schema.json for theme_info block."""
|
|
22
|
+
schema_path = theme_path / "config" / "settings_schema.json"
|
|
23
|
+
defaults = {"name": "Unknown", "version": "0.0.0", "author": "Unknown"}
|
|
24
|
+
if not schema_path.exists():
|
|
25
|
+
return defaults
|
|
26
|
+
try:
|
|
27
|
+
data = json.loads(schema_path.read_text(encoding="utf-8"))
|
|
28
|
+
if not isinstance(data, list):
|
|
29
|
+
return defaults
|
|
30
|
+
for item in data:
|
|
31
|
+
if isinstance(item, dict) and "theme_info" in item:
|
|
32
|
+
info = item["theme_info"]
|
|
33
|
+
return {"name": info.get("theme_name", "Unknown"),
|
|
34
|
+
"version": info.get("theme_version", "0.0.0"),
|
|
35
|
+
"author": info.get("theme_author", "Unknown")}
|
|
36
|
+
first = data[0] if data else {}
|
|
37
|
+
if isinstance(first, dict) and "name" in first:
|
|
38
|
+
return {"name": first["name"],
|
|
39
|
+
"version": first.get("theme_version", "0.0.0"),
|
|
40
|
+
"author": first.get("theme_author", "Unknown")}
|
|
41
|
+
except (json.JSONDecodeError, KeyError, IndexError):
|
|
42
|
+
pass
|
|
43
|
+
return defaults
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def scan_css_variables(theme_path: Path, max_files: int = 50) -> dict:
|
|
47
|
+
"""Scan assets/*.css for CSS custom property prefixes, return top 10 by count."""
|
|
48
|
+
assets_dir = theme_path / "assets"
|
|
49
|
+
if not assets_dir.exists():
|
|
50
|
+
return {}
|
|
51
|
+
var_pattern = re.compile(r"--([\w]+)-")
|
|
52
|
+
counts: dict = {}
|
|
53
|
+
for css_file in list(assets_dir.glob("*.css"))[:max_files]:
|
|
54
|
+
try:
|
|
55
|
+
content = css_file.read_text(encoding="utf-8", errors="ignore")
|
|
56
|
+
for m in var_pattern.finditer(content):
|
|
57
|
+
p = m.group(1)
|
|
58
|
+
counts[p] = counts.get(p, 0) + 1
|
|
59
|
+
except Exception:
|
|
60
|
+
continue
|
|
61
|
+
return dict(sorted(counts.items(), key=lambda x: x[1], reverse=True)[:10])
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def analyze_sections(theme_path: Path) -> dict:
|
|
65
|
+
"""List sections, extract naming conventions, count files."""
|
|
66
|
+
sections_dir = theme_path / "sections"
|
|
67
|
+
if not sections_dir.exists():
|
|
68
|
+
return {"count": 0, "patterns": [], "files": []}
|
|
69
|
+
files = [f.stem for f in sections_dir.glob("*.liquid")]
|
|
70
|
+
patterns: set = set()
|
|
71
|
+
for name in files:
|
|
72
|
+
if name.startswith("main-"):
|
|
73
|
+
patterns.add("main-* prefix")
|
|
74
|
+
if name.startswith("featured-"):
|
|
75
|
+
patterns.add("featured-* prefix")
|
|
76
|
+
if "-" in name:
|
|
77
|
+
patterns.add("kebab-case")
|
|
78
|
+
elif "_" in name:
|
|
79
|
+
patterns.add("snake_case")
|
|
80
|
+
return {"count": len(files), "patterns": sorted(patterns), "files": files[:20]}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def match_theme_dna(theme_info: dict, dna_path: Path) -> dict | None:
|
|
84
|
+
"""Match detected theme against theme-dna.csv by name then author+keyword."""
|
|
85
|
+
if not dna_path.exists():
|
|
86
|
+
return None
|
|
87
|
+
name_l = theme_info["name"].lower()
|
|
88
|
+
author_l = theme_info["author"].lower()
|
|
89
|
+
try:
|
|
90
|
+
with open(dna_path, "r", encoding="utf-8") as f:
|
|
91
|
+
rows = list(csv.DictReader(f))
|
|
92
|
+
for row in rows:
|
|
93
|
+
if name_l in row.get("Theme", "").lower():
|
|
94
|
+
return row
|
|
95
|
+
for row in rows:
|
|
96
|
+
if (author_l in row.get("Publisher", "").lower()
|
|
97
|
+
and any(name_l in kw for kw in row.get("Keywords", "").lower().split(","))):
|
|
98
|
+
return row
|
|
99
|
+
except Exception:
|
|
100
|
+
pass
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def generate_profile(theme_info: dict, css_vars: dict, sections: dict,
|
|
105
|
+
dna: dict | None, output_path: str | None) -> str:
|
|
106
|
+
"""Build and optionally write theme-profile.md content."""
|
|
107
|
+
d = dna or {}
|
|
108
|
+
top_prefix = f"--{list(css_vars.keys())[0]}-" if css_vars else "--"
|
|
109
|
+
detected_colors = ", ".join(f"--{k}-*" for k in list(css_vars.keys())[:5]) or "Not detected"
|
|
110
|
+
patterns_str = ", ".join(sections["patterns"]) if sections["patterns"] else "N/A"
|
|
111
|
+
|
|
112
|
+
lines = [
|
|
113
|
+
f"# Theme Profile: {theme_info['name']}",
|
|
114
|
+
f"Generated: {date.today()} | Version: {theme_info['version']} | Author: {theme_info['author']}",
|
|
115
|
+
"",
|
|
116
|
+
"## Conventions",
|
|
117
|
+
f"- CSS Variable Prefix: `{d.get('CSS Variable Prefix', top_prefix)}`" + ("" if dna else " (detected)"),
|
|
118
|
+
f"- Section Naming: {d.get('Section Naming', 'kebab-case')}",
|
|
119
|
+
f"- Block Convention: {d.get('Block Conventions', 'standard OS2.0')}",
|
|
120
|
+
"",
|
|
121
|
+
"## Design Tokens (from theme)",
|
|
122
|
+
f"- Color Scheme: {d.get('Color Scheme Pattern', detected_colors)}",
|
|
123
|
+
f"- Typography: {d.get('Typography Pattern', 'Use theme defaults')}",
|
|
124
|
+
f"- Spacing: {d.get('Spacing Pattern', 'Use theme defaults')}",
|
|
125
|
+
"",
|
|
126
|
+
"## Section Patterns",
|
|
127
|
+
f"- Detected sections: {sections['count']}",
|
|
128
|
+
f"- Naming patterns: {patterns_str}",
|
|
129
|
+
"",
|
|
130
|
+
"## Recommendations",
|
|
131
|
+
]
|
|
132
|
+
if dna:
|
|
133
|
+
lines += [
|
|
134
|
+
f"- Follow {theme_info['name']} naming conventions",
|
|
135
|
+
f"- Use `{d.get('CSS Variable Prefix', '--')}` prefix for CSS variables",
|
|
136
|
+
f"- Match {d.get('Color Scheme Pattern', 'existing')} color scheme pattern",
|
|
137
|
+
]
|
|
138
|
+
else:
|
|
139
|
+
lines += [
|
|
140
|
+
"- Use kebab-case for section file names",
|
|
141
|
+
"- Scope CSS with #shopify-section-{{ section.id }}",
|
|
142
|
+
"- Follow OS2.0 standards (presets, @app blocks)",
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
content = "\n".join(lines) + "\n"
|
|
146
|
+
if output_path:
|
|
147
|
+
Path(output_path).write_text(content, encoding="utf-8")
|
|
148
|
+
print(f"Theme profile saved to: {output_path}")
|
|
149
|
+
return content
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def main() -> None:
|
|
153
|
+
parser = argparse.ArgumentParser(
|
|
154
|
+
description="Rebly Sections Theme Init — scan a Shopify theme and generate a profile.")
|
|
155
|
+
parser.add_argument("theme_path", help="Path to Shopify theme directory")
|
|
156
|
+
parser.add_argument("--output", "-o", default=None, help="Output path for theme-profile.md")
|
|
157
|
+
args = parser.parse_args()
|
|
158
|
+
|
|
159
|
+
theme_path = Path(args.theme_path)
|
|
160
|
+
if not theme_path.exists():
|
|
161
|
+
print(f"Error: Theme path not found: {theme_path}", file=sys.stderr)
|
|
162
|
+
sys.exit(1)
|
|
163
|
+
|
|
164
|
+
theme_info = detect_theme_info(theme_path)
|
|
165
|
+
print(f"Detected theme: {theme_info['name']} v{theme_info['version']} by {theme_info['author']}")
|
|
166
|
+
|
|
167
|
+
css_vars = scan_css_variables(theme_path)
|
|
168
|
+
print(f"CSS variable prefixes: {', '.join(f'--{k}-({v})' for k, v in list(css_vars.items())[:5]) or 'none detected'}")
|
|
169
|
+
|
|
170
|
+
sections = analyze_sections(theme_path)
|
|
171
|
+
print(f"Sections found: {sections['count']}")
|
|
172
|
+
|
|
173
|
+
dna_match = match_theme_dna(theme_info, DATA_DIR / "theme-dna.csv")
|
|
174
|
+
print(f"Theme DNA match: {dna_match.get('Theme', 'Unknown')}" if dna_match else "No exact DNA match — using generic OS2.0 profile")
|
|
175
|
+
|
|
176
|
+
profile = generate_profile(theme_info, css_vars, sections, dna_match, args.output or "theme-profile.md")
|
|
177
|
+
print(f"\n{profile}")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
if __name__ == "__main__":
|
|
181
|
+
main()
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Generate Shopify Section: {{ name }}
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
{{ description }}
|
|
5
|
+
|
|
6
|
+
## Theme Context
|
|
7
|
+
{{ theme_profile_summary }}
|
|
8
|
+
|
|
9
|
+
## Closest Pattern Template
|
|
10
|
+
{{ component_pattern_content }}
|
|
11
|
+
|
|
12
|
+
## Relevant Schema Settings
|
|
13
|
+
{{ schema_search_results }}
|
|
14
|
+
|
|
15
|
+
## Applicable Best Practices
|
|
16
|
+
{{ best_practices_results }}
|
|
17
|
+
|
|
18
|
+
## Design Recommendations
|
|
19
|
+
{{ ui_ux_recommendations }}
|
|
20
|
+
|
|
21
|
+
## Base Template
|
|
22
|
+
{{ section_base_content }}
|
|
23
|
+
|
|
24
|
+
## Requirements
|
|
25
|
+
- OS2.0 compliant (presets, @app blocks, block.shopify_attributes)
|
|
26
|
+
- Scoped CSS via #section-{{ section.id }}
|
|
27
|
+
- Responsive (mobile-first)
|
|
28
|
+
- Accessible (semantic HTML, aria-labels, alt text)
|
|
29
|
+
- Performance (lazy-load non-hero images, no Liquid in CSS/JS)
|
|
30
|
+
- Use {% render %} not {% include %}
|
|
31
|
+
- Include {{ block.shopify_attributes }} on ALL block wrappers
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{%- comment -%}
|
|
2
|
+
rebly-sections: {{ name }}
|
|
3
|
+
Generated: {{ date }}
|
|
4
|
+
Theme: {{ theme_name }}
|
|
5
|
+
{%- endcomment -%}
|
|
6
|
+
|
|
7
|
+
{%- liquid
|
|
8
|
+
assign section_id = section.id
|
|
9
|
+
-%}
|
|
10
|
+
|
|
11
|
+
<section
|
|
12
|
+
id="section-{{ section_id }}"
|
|
13
|
+
class="rebly-{{ slug }} section-{{ section_id }}"
|
|
14
|
+
>
|
|
15
|
+
<div class="page-width">
|
|
16
|
+
{%- comment -%} Section content goes here {%- endcomment -%}
|
|
17
|
+
|
|
18
|
+
{%- for block in section.blocks -%}
|
|
19
|
+
{%- case block.type -%}
|
|
20
|
+
{%- when '@app' -%}
|
|
21
|
+
<div {{ block.shopify_attributes }}>
|
|
22
|
+
{% render block %}
|
|
23
|
+
</div>
|
|
24
|
+
{%- comment -%} Custom block types go here {%- endcomment -%}
|
|
25
|
+
{%- endcase -%}
|
|
26
|
+
{%- endfor -%}
|
|
27
|
+
</div>
|
|
28
|
+
</section>
|
|
29
|
+
|
|
30
|
+
<style>
|
|
31
|
+
#section-{{ section_id }} {
|
|
32
|
+
padding: var(--section-padding-top) 0 var(--section-padding-bottom);
|
|
33
|
+
}
|
|
34
|
+
</style>
|
|
35
|
+
|
|
36
|
+
{% schema %}
|
|
37
|
+
{
|
|
38
|
+
"name": "{{ display_name }}",
|
|
39
|
+
"tag": "section",
|
|
40
|
+
"class": "section",
|
|
41
|
+
"settings": [],
|
|
42
|
+
"blocks": [
|
|
43
|
+
{ "type": "@app" }
|
|
44
|
+
],
|
|
45
|
+
"presets": [
|
|
46
|
+
{ "name": "{{ display_name }}" }
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
{% endschema %}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Theme Profile: {theme_name}
|
|
2
|
+
Generated: {date} | Version: {version} | Author: {author}
|
|
3
|
+
|
|
4
|
+
## Conventions
|
|
5
|
+
- CSS Variable Prefix: `{css_prefix}`
|
|
6
|
+
- Section Naming: {naming_convention}
|
|
7
|
+
- Block Convention: {block_convention}
|
|
8
|
+
|
|
9
|
+
## Design Tokens (from theme)
|
|
10
|
+
- Color Scheme: {color_scheme}
|
|
11
|
+
- Typography: {typography}
|
|
12
|
+
- Spacing: {spacing}
|
|
13
|
+
|
|
14
|
+
## Section Patterns
|
|
15
|
+
- Detected sections: {section_count}
|
|
16
|
+
- Naming patterns: {naming_patterns}
|
|
17
|
+
|
|
18
|
+
## Recommendations
|
|
19
|
+
- {rec_1}
|
|
20
|
+
- {rec_2}
|
|
21
|
+
- {rec_3}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function init(cwd: string): void;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { execSync } from 'node:child_process';
|
|
5
|
+
import { detectPlatforms } from '../utils/detect-platform.js';
|
|
6
|
+
import { copyAssets } from '../utils/copy-assets.js';
|
|
7
|
+
export function init(cwd) {
|
|
8
|
+
console.log('rebly-sections installer\n');
|
|
9
|
+
const targets = detectPlatforms(cwd);
|
|
10
|
+
if (targets.length === 0) {
|
|
11
|
+
console.log('No .claude/ or .agent/ directory detected.');
|
|
12
|
+
console.log('');
|
|
13
|
+
console.log('To use with Claude Code:');
|
|
14
|
+
console.log(' mkdir -p .claude/skills');
|
|
15
|
+
console.log(' npx rebly-sections init');
|
|
16
|
+
console.log('');
|
|
17
|
+
console.log('To use with Antigravity Kit:');
|
|
18
|
+
console.log(' mkdir -p .agent/skills');
|
|
19
|
+
console.log(' npx rebly-sections init');
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
// In dist/commands/init.js -> assets is at ../../assets/
|
|
23
|
+
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
const assetsDir = join(scriptDir, '..', '..', 'assets');
|
|
25
|
+
if (!existsSync(assetsDir)) {
|
|
26
|
+
console.error('Error: Assets directory not found. Package may be corrupted.');
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
const commandsDir = join(assetsDir, 'commands');
|
|
30
|
+
for (const target of targets) {
|
|
31
|
+
console.log(`Installing to ${target.platform}...`);
|
|
32
|
+
console.log(` Target: ${target.targetDir}`);
|
|
33
|
+
try {
|
|
34
|
+
const fileCount = copyAssets(assetsDir, target.targetDir);
|
|
35
|
+
console.log(` Copied ${fileCount} files`);
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
console.error(` Error: ${err instanceof Error ? err.message : err}`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
// Install slash commands to .claude/commands/ or .agent/commands/
|
|
42
|
+
if (existsSync(commandsDir)) {
|
|
43
|
+
const platformDir = join(cwd, target.platform === 'claude' ? '.claude' : '.agent');
|
|
44
|
+
const targetCommandsDir = join(platformDir, 'commands');
|
|
45
|
+
try {
|
|
46
|
+
const cmdCount = copyAssets(commandsDir, targetCommandsDir);
|
|
47
|
+
console.log(` Installed ${cmdCount} slash commands (/theme-init, /section)\n`);
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
console.error(` Warning: Could not install slash commands: ${err instanceof Error ? err.message : err}\n`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
console.log('Checking Python...');
|
|
55
|
+
try {
|
|
56
|
+
const version = execSync('python3 --version', { encoding: 'utf-8' }).trim();
|
|
57
|
+
console.log(` ${version}`);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
console.log(' Warning: python3 not found. Python 3.x is required for search scripts.');
|
|
61
|
+
}
|
|
62
|
+
console.log('\nInstallation complete!\n');
|
|
63
|
+
console.log('Usage:');
|
|
64
|
+
console.log(' /theme-init — Scan your Shopify theme');
|
|
65
|
+
console.log(' /section — Generate a new section');
|
|
66
|
+
console.log('');
|
|
67
|
+
console.log('Search:');
|
|
68
|
+
console.log(' python3 scripts/search.py "hero section" --component');
|
|
69
|
+
console.log(' python3 scripts/search.py "image_picker" --domain schema');
|
|
70
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { init } from './commands/init.js';
|
|
3
|
+
const args = process.argv.slice(2);
|
|
4
|
+
const command = args[0];
|
|
5
|
+
if (command === 'init') {
|
|
6
|
+
init(process.cwd());
|
|
7
|
+
}
|
|
8
|
+
else {
|
|
9
|
+
console.log('rebly-sections - Shopify section AI skill installer\n');
|
|
10
|
+
console.log('Usage: npx rebly-sections init\n');
|
|
11
|
+
console.log('Commands:');
|
|
12
|
+
console.log(' init Install rebly-sections skill to detected platform(s)');
|
|
13
|
+
process.exit(command ? 1 : 0);
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function copyAssets(assetsDir: string, targetDir: string): number;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { cpSync, existsSync, mkdirSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
const SKIP_PATTERNS = ['.gitkeep', '__pycache__', '.pyc', '.DS_Store', 'commands'];
|
|
4
|
+
function shouldSkip(name) {
|
|
5
|
+
return SKIP_PATTERNS.some(p => name.includes(p));
|
|
6
|
+
}
|
|
7
|
+
export function copyAssets(assetsDir, targetDir) {
|
|
8
|
+
if (!existsSync(assetsDir)) {
|
|
9
|
+
throw new Error(`Assets directory not found: ${assetsDir}`);
|
|
10
|
+
}
|
|
11
|
+
if (!existsSync(targetDir)) {
|
|
12
|
+
mkdirSync(targetDir, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
let fileCount = 0;
|
|
15
|
+
const entries = readdirSync(assetsDir, { withFileTypes: true });
|
|
16
|
+
for (const entry of entries) {
|
|
17
|
+
if (shouldSkip(entry.name))
|
|
18
|
+
continue;
|
|
19
|
+
const srcPath = join(assetsDir, entry.name);
|
|
20
|
+
const destPath = join(targetDir, entry.name);
|
|
21
|
+
if (entry.isDirectory()) {
|
|
22
|
+
cpSync(srcPath, destPath, { recursive: true, filter: (src) => !shouldSkip(src) });
|
|
23
|
+
fileCount += countFiles(destPath);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
cpSync(srcPath, destPath);
|
|
27
|
+
fileCount++;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return fileCount;
|
|
31
|
+
}
|
|
32
|
+
function countFiles(dir) {
|
|
33
|
+
let count = 0;
|
|
34
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
if (entry.isDirectory()) {
|
|
37
|
+
count += countFiles(join(dir, entry.name));
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
count++;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return count;
|
|
44
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
export function detectPlatforms(cwd) {
|
|
4
|
+
const targets = [];
|
|
5
|
+
if (existsSync(join(cwd, '.claude'))) {
|
|
6
|
+
targets.push({
|
|
7
|
+
platform: 'claude',
|
|
8
|
+
targetDir: join(cwd, '.claude', 'skills', 'rebly-sections')
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
if (existsSync(join(cwd, '.agent'))) {
|
|
12
|
+
targets.push({
|
|
13
|
+
platform: 'antigravity',
|
|
14
|
+
targetDir: join(cwd, '.agent', 'skills', 'rebly-sections')
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return targets;
|
|
18
|
+
}
|