424 lines
12 KiB
Python
424 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Shopify Project Initialization Script
|
|
|
|
Interactive script to scaffold Shopify apps, extensions, or themes.
|
|
Supports environment variable loading from multiple locations.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import json
|
|
import subprocess
|
|
from pathlib import Path
|
|
from typing import Dict, Optional, List
|
|
from dataclasses import dataclass
|
|
|
|
|
|
@dataclass
|
|
class EnvConfig:
|
|
"""Environment configuration container."""
|
|
shopify_api_key: Optional[str] = None
|
|
shopify_api_secret: Optional[str] = None
|
|
shop_domain: Optional[str] = None
|
|
scopes: Optional[str] = None
|
|
|
|
|
|
class EnvLoader:
|
|
"""Load environment variables from multiple sources in priority order."""
|
|
|
|
@staticmethod
|
|
def load_env_file(filepath: Path) -> Dict[str, str]:
|
|
"""
|
|
Load environment variables from .env file.
|
|
|
|
Args:
|
|
filepath: Path to .env file
|
|
|
|
Returns:
|
|
Dictionary of environment variables
|
|
"""
|
|
env_vars = {}
|
|
if not filepath.exists():
|
|
return env_vars
|
|
|
|
try:
|
|
with open(filepath, 'r') as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if line and not line.startswith('#') and '=' in line:
|
|
key, value = line.split('=', 1)
|
|
env_vars[key.strip()] = value.strip().strip('"').strip("'")
|
|
except Exception as e:
|
|
print(f"Warning: Failed to load {filepath}: {e}")
|
|
|
|
return env_vars
|
|
|
|
@staticmethod
|
|
def get_env_paths(skill_dir: Path) -> List[Path]:
|
|
"""
|
|
Get list of .env file paths in priority order.
|
|
|
|
Priority: process.env > skill/.env > skills/.env > .claude/.env
|
|
|
|
Args:
|
|
skill_dir: Path to skill directory
|
|
|
|
Returns:
|
|
List of .env file paths
|
|
"""
|
|
paths = []
|
|
|
|
# skill/.env
|
|
skill_env = skill_dir / '.env'
|
|
if skill_env.exists():
|
|
paths.append(skill_env)
|
|
|
|
# skills/.env
|
|
skills_env = skill_dir.parent / '.env'
|
|
if skills_env.exists():
|
|
paths.append(skills_env)
|
|
|
|
# .claude/.env
|
|
claude_env = skill_dir.parent.parent / '.env'
|
|
if claude_env.exists():
|
|
paths.append(claude_env)
|
|
|
|
return paths
|
|
|
|
@staticmethod
|
|
def load_config(skill_dir: Path) -> EnvConfig:
|
|
"""
|
|
Load configuration from environment variables.
|
|
|
|
Priority: process.env > skill/.env > skills/.env > .claude/.env
|
|
|
|
Args:
|
|
skill_dir: Path to skill directory
|
|
|
|
Returns:
|
|
EnvConfig object
|
|
"""
|
|
config = EnvConfig()
|
|
|
|
# Load from .env files (reverse priority order)
|
|
for env_path in reversed(EnvLoader.get_env_paths(skill_dir)):
|
|
env_vars = EnvLoader.load_env_file(env_path)
|
|
if 'SHOPIFY_API_KEY' in env_vars:
|
|
config.shopify_api_key = env_vars['SHOPIFY_API_KEY']
|
|
if 'SHOPIFY_API_SECRET' in env_vars:
|
|
config.shopify_api_secret = env_vars['SHOPIFY_API_SECRET']
|
|
if 'SHOP_DOMAIN' in env_vars:
|
|
config.shop_domain = env_vars['SHOP_DOMAIN']
|
|
if 'SCOPES' in env_vars:
|
|
config.scopes = env_vars['SCOPES']
|
|
|
|
# Override with process environment (highest priority)
|
|
if 'SHOPIFY_API_KEY' in os.environ:
|
|
config.shopify_api_key = os.environ['SHOPIFY_API_KEY']
|
|
if 'SHOPIFY_API_SECRET' in os.environ:
|
|
config.shopify_api_secret = os.environ['SHOPIFY_API_SECRET']
|
|
if 'SHOP_DOMAIN' in os.environ:
|
|
config.shop_domain = os.environ['SHOP_DOMAIN']
|
|
if 'SCOPES' in os.environ:
|
|
config.scopes = os.environ['SCOPES']
|
|
|
|
return config
|
|
|
|
|
|
class ShopifyInitializer:
|
|
"""Initialize Shopify projects."""
|
|
|
|
def __init__(self, config: EnvConfig):
|
|
"""
|
|
Initialize ShopifyInitializer.
|
|
|
|
Args:
|
|
config: Environment configuration
|
|
"""
|
|
self.config = config
|
|
|
|
def prompt(self, message: str, default: Optional[str] = None) -> str:
|
|
"""
|
|
Prompt user for input.
|
|
|
|
Args:
|
|
message: Prompt message
|
|
default: Default value
|
|
|
|
Returns:
|
|
User input or default
|
|
"""
|
|
if default:
|
|
message = f"{message} [{default}]"
|
|
user_input = input(f"{message}: ").strip()
|
|
return user_input if user_input else (default or '')
|
|
|
|
def select_option(self, message: str, options: List[str]) -> str:
|
|
"""
|
|
Prompt user to select from options.
|
|
|
|
Args:
|
|
message: Prompt message
|
|
options: List of options
|
|
|
|
Returns:
|
|
Selected option
|
|
"""
|
|
print(f"\n{message}")
|
|
for i, option in enumerate(options, 1):
|
|
print(f"{i}. {option}")
|
|
|
|
while True:
|
|
try:
|
|
choice = int(input("Select option: ").strip())
|
|
if 1 <= choice <= len(options):
|
|
return options[choice - 1]
|
|
print(f"Please select 1-{len(options)}")
|
|
except (ValueError, KeyboardInterrupt):
|
|
print("Invalid input")
|
|
|
|
def check_cli_installed(self) -> bool:
|
|
"""
|
|
Check if Shopify CLI is installed.
|
|
|
|
Returns:
|
|
True if installed, False otherwise
|
|
"""
|
|
try:
|
|
result = subprocess.run(
|
|
['shopify', 'version'],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=5
|
|
)
|
|
return result.returncode == 0
|
|
except (subprocess.SubprocessError, FileNotFoundError):
|
|
return False
|
|
|
|
def create_app_config(self, project_dir: Path, app_name: str, scopes: str) -> None:
|
|
"""
|
|
Create shopify.app.toml configuration file.
|
|
|
|
Args:
|
|
project_dir: Project directory
|
|
app_name: Application name
|
|
scopes: Access scopes
|
|
"""
|
|
config_content = f"""# Shopify App Configuration
|
|
name = "{app_name}"
|
|
client_id = "{self.config.shopify_api_key or 'YOUR_API_KEY'}"
|
|
application_url = "https://your-app.com"
|
|
embedded = true
|
|
|
|
[build]
|
|
automatically_update_urls_on_dev = true
|
|
dev_store_url = "{self.config.shop_domain or 'your-store.myshopify.com'}"
|
|
|
|
[access_scopes]
|
|
scopes = "{scopes}"
|
|
|
|
[webhooks]
|
|
api_version = "2025-01"
|
|
|
|
[[webhooks.subscriptions]]
|
|
topics = ["app/uninstalled"]
|
|
uri = "/webhooks/app/uninstalled"
|
|
|
|
[webhooks.privacy_compliance]
|
|
customer_data_request_url = "/webhooks/gdpr/data-request"
|
|
customer_deletion_url = "/webhooks/gdpr/customer-deletion"
|
|
shop_deletion_url = "/webhooks/gdpr/shop-deletion"
|
|
"""
|
|
config_path = project_dir / 'shopify.app.toml'
|
|
config_path.write_text(config_content)
|
|
print(f"✓ Created {config_path}")
|
|
|
|
def create_extension_config(self, project_dir: Path, extension_name: str, extension_type: str) -> None:
|
|
"""
|
|
Create shopify.extension.toml configuration file.
|
|
|
|
Args:
|
|
project_dir: Project directory
|
|
extension_name: Extension name
|
|
extension_type: Extension type
|
|
"""
|
|
target_map = {
|
|
'checkout': 'purchase.checkout.block.render',
|
|
'admin_action': 'admin.product-details.action.render',
|
|
'admin_block': 'admin.product-details.block.render',
|
|
'pos': 'pos.home.tile.render'
|
|
}
|
|
|
|
config_content = f"""name = "{extension_name}"
|
|
type = "ui_extension"
|
|
handle = "{extension_name.lower().replace(' ', '-')}"
|
|
|
|
[extension_points]
|
|
api_version = "2025-01"
|
|
|
|
[[extension_points.targets]]
|
|
target = "{target_map.get(extension_type, 'purchase.checkout.block.render')}"
|
|
|
|
[capabilities]
|
|
network_access = true
|
|
api_access = true
|
|
"""
|
|
config_path = project_dir / 'shopify.extension.toml'
|
|
config_path.write_text(config_content)
|
|
print(f"✓ Created {config_path}")
|
|
|
|
def create_readme(self, project_dir: Path, project_type: str, project_name: str) -> None:
|
|
"""
|
|
Create README.md file.
|
|
|
|
Args:
|
|
project_dir: Project directory
|
|
project_type: Project type (app/extension/theme)
|
|
project_name: Project name
|
|
"""
|
|
content = f"""# {project_name}
|
|
|
|
Shopify {project_type.capitalize()} project.
|
|
|
|
## Setup
|
|
|
|
```bash
|
|
# Install dependencies
|
|
npm install
|
|
|
|
# Start development
|
|
shopify {project_type} dev
|
|
```
|
|
|
|
## Deployment
|
|
|
|
```bash
|
|
# Deploy to Shopify
|
|
shopify {project_type} deploy
|
|
```
|
|
|
|
## Resources
|
|
|
|
- [Shopify Documentation](https://shopify.dev/docs)
|
|
- [Shopify CLI](https://shopify.dev/docs/api/shopify-cli)
|
|
"""
|
|
readme_path = project_dir / 'README.md'
|
|
readme_path.write_text(content)
|
|
print(f"✓ Created {readme_path}")
|
|
|
|
def init_app(self) -> None:
|
|
"""Initialize Shopify app project."""
|
|
print("\n=== Shopify App Initialization ===\n")
|
|
|
|
app_name = self.prompt("App name", "my-shopify-app")
|
|
scopes = self.prompt("Access scopes", self.config.scopes or "read_products,write_products")
|
|
|
|
project_dir = Path.cwd() / app_name
|
|
project_dir.mkdir(exist_ok=True)
|
|
|
|
print(f"\nCreating app in {project_dir}...")
|
|
|
|
self.create_app_config(project_dir, app_name, scopes)
|
|
self.create_readme(project_dir, "app", app_name)
|
|
|
|
# Create basic package.json
|
|
package_json = {
|
|
"name": app_name.lower().replace(' ', '-'),
|
|
"version": "1.0.0",
|
|
"scripts": {
|
|
"dev": "shopify app dev",
|
|
"deploy": "shopify app deploy"
|
|
}
|
|
}
|
|
(project_dir / 'package.json').write_text(json.dumps(package_json, indent=2))
|
|
print(f"✓ Created package.json")
|
|
|
|
print(f"\n✓ App '{app_name}' initialized successfully!")
|
|
print(f"\nNext steps:")
|
|
print(f" cd {app_name}")
|
|
print(f" npm install")
|
|
print(f" shopify app dev")
|
|
|
|
def init_extension(self) -> None:
|
|
"""Initialize Shopify extension project."""
|
|
print("\n=== Shopify Extension Initialization ===\n")
|
|
|
|
extension_types = ['checkout', 'admin_action', 'admin_block', 'pos']
|
|
extension_type = self.select_option("Select extension type", extension_types)
|
|
|
|
extension_name = self.prompt("Extension name", "my-extension")
|
|
|
|
project_dir = Path.cwd() / extension_name
|
|
project_dir.mkdir(exist_ok=True)
|
|
|
|
print(f"\nCreating extension in {project_dir}...")
|
|
|
|
self.create_extension_config(project_dir, extension_name, extension_type)
|
|
self.create_readme(project_dir, "extension", extension_name)
|
|
|
|
print(f"\n✓ Extension '{extension_name}' initialized successfully!")
|
|
print(f"\nNext steps:")
|
|
print(f" cd {extension_name}")
|
|
print(f" shopify app dev")
|
|
|
|
def init_theme(self) -> None:
|
|
"""Initialize Shopify theme project."""
|
|
print("\n=== Shopify Theme Initialization ===\n")
|
|
|
|
theme_name = self.prompt("Theme name", "my-theme")
|
|
|
|
print(f"\nInitializing theme '{theme_name}'...")
|
|
print("\nRecommended: Use 'shopify theme init' for full theme scaffolding")
|
|
print(f"\nRun: shopify theme init {theme_name}")
|
|
|
|
def run(self) -> None:
|
|
"""Run interactive initialization."""
|
|
print("=" * 60)
|
|
print("Shopify Project Initializer")
|
|
print("=" * 60)
|
|
|
|
# Check CLI
|
|
if not self.check_cli_installed():
|
|
print("\n⚠ Shopify CLI not found!")
|
|
print("Install: npm install -g @shopify/cli@latest")
|
|
sys.exit(1)
|
|
|
|
# Select project type
|
|
project_types = ['app', 'extension', 'theme']
|
|
project_type = self.select_option("Select project type", project_types)
|
|
|
|
# Initialize based on type
|
|
if project_type == 'app':
|
|
self.init_app()
|
|
elif project_type == 'extension':
|
|
self.init_extension()
|
|
elif project_type == 'theme':
|
|
self.init_theme()
|
|
|
|
|
|
def main() -> None:
|
|
"""Main entry point."""
|
|
try:
|
|
# Get skill directory
|
|
script_dir = Path(__file__).parent
|
|
skill_dir = script_dir.parent
|
|
|
|
# Load configuration
|
|
config = EnvLoader.load_config(skill_dir)
|
|
|
|
# Initialize project
|
|
initializer = ShopifyInitializer(config)
|
|
initializer.run()
|
|
|
|
except KeyboardInterrupt:
|
|
print("\n\nAborted.")
|
|
sys.exit(0)
|
|
except Exception as e:
|
|
print(f"\n✗ Error: {e}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|