""" Design management commands for mycopunk CLI. """ import os from pathlib import Path from typing import Optional from datetime import date import yaml from rich.console import Console from rich.table import Table from rich.panel import Panel console = Console() # Project root detection def get_project_root() -> Path: """Find the project root (directory containing designs/).""" current = Path.cwd() while current != current.parent: if (current / "designs").is_dir(): return current current = current.parent # Default to cwd if not found return Path.cwd() DESIGNS_DIR = get_project_root() / "designs" TEMPLATES_DIR = get_project_root() / "templates" # Default metadata template METADATA_TEMPLATE = """name: "{name}" slug: {slug} description: "Description of your design" tags: [mycopunk, mycelium] created: {date} author: jeff source: file: {slug}.svg format: svg dimensions: width: {width} height: {height} dpi: 300 color_profile: sRGB products: {products} status: draft """ # Product templates by type PRODUCT_TEMPLATES = { "sticker-3x3": { "width": 900, "height": 900, "products": """ - type: sticker provider: prodigi sku: GLOBAL-STI-KIS-3X3 variants: [matte, gloss] retail_price: 3.50""", }, "sticker-4x4": { "width": 1200, "height": 1200, "products": """ - type: sticker provider: prodigi sku: GLOBAL-STI-KIS-4X4 variants: [matte, gloss] retail_price: 4.50""", }, "sticker-6x6": { "width": 1800, "height": 1800, "products": """ - type: sticker provider: prodigi sku: GLOBAL-STI-KIS-6X6 variants: [matte] retail_price: 6.00""", }, "tshirt-front": { "width": 3600, "height": 4800, "products": """ - type: tshirt provider: printful sku: 71 placement: front colors: [black, white, heather_charcoal] sizes: [S, M, L, XL, 2XL] retail_price: 28.00""", }, "tshirt-chest": { "width": 1200, "height": 1200, "products": """ - type: tshirt provider: printful sku: 71 placement: chest colors: [black, white] sizes: [S, M, L, XL, 2XL] retail_price: 25.00""", }, "print-8x10": { "width": 2400, "height": 3000, "products": """ - type: print provider: prodigi sku: GLOBAL-FAP-8X10 variants: [matte, lustre] retail_price: 15.00""", }, } def create_design(path: str, template: str) -> None: """Create a new design scaffold.""" # Parse path parts = path.split("/") if len(parts) < 2: console.print("[red]Error: Path must be category/design-name (e.g., stickers/my-design)[/red]") raise SystemExit(1) category = parts[0] slug = parts[-1] name = slug.replace("-", " ").title() # Validate template if template not in PRODUCT_TEMPLATES: console.print(f"[red]Error: Unknown template '{template}'[/red]") console.print(f"Available templates: {', '.join(PRODUCT_TEMPLATES.keys())}") raise SystemExit(1) template_config = PRODUCT_TEMPLATES[template] # Create directory structure design_dir = DESIGNS_DIR / category / slug exports_dir = design_dir / "exports" / "300dpi" mockups_dir = design_dir / "exports" / "mockups" if design_dir.exists(): console.print(f"[red]Error: Design already exists at {design_dir}[/red]") raise SystemExit(1) design_dir.mkdir(parents=True, exist_ok=True) exports_dir.mkdir(parents=True, exist_ok=True) mockups_dir.mkdir(parents=True, exist_ok=True) # Create metadata.yaml metadata_content = METADATA_TEMPLATE.format( name=name, slug=slug, date=date.today().isoformat(), width=template_config["width"], height=template_config["height"], products=template_config["products"], ) metadata_path = design_dir / "metadata.yaml" metadata_path.write_text(metadata_content) # Create placeholder SVG svg_path = design_dir / f"{slug}.svg" svg_content = f''' {name} ''' svg_path.write_text(svg_content) # Create .gitkeep in exports (exports_dir / ".gitkeep").touch() (mockups_dir / ".gitkeep").touch() console.print(Panel( f"""[green]✓ Created design scaffold[/green] [bold]Location:[/bold] {design_dir} [bold]Template:[/bold] {template} [bold]Canvas:[/bold] {template_config["width"]}×{template_config["height"]} px [bold]Files created:[/bold] • {slug}.svg - Edit this in Inkscape • metadata.yaml - Configure products • exports/ - Generated files go here [bold]Next steps:[/bold] 1. Open {svg_path} in Inkscape 2. Create your design 3. Edit metadata.yaml to configure products 4. Run: mycopunk design export {path}""", title="🍄 New Design", )) def list_designs(status: Optional[str] = None, design_type: Optional[str] = None) -> None: """List all designs in the repository.""" table = Table(title="🍄 Mycopunk Designs") table.add_column("Path", style="cyan") table.add_column("Name", style="green") table.add_column("Status", style="yellow") table.add_column("Products", style="blue") table.add_column("Created", style="dim") designs_found = 0 for category_dir in DESIGNS_DIR.iterdir(): if not category_dir.is_dir(): continue # Filter by type if design_type and category_dir.name != design_type: continue for design_dir in category_dir.iterdir(): if not design_dir.is_dir(): continue metadata_path = design_dir / "metadata.yaml" if not metadata_path.exists(): continue try: with open(metadata_path) as f: metadata = yaml.safe_load(f) except Exception: continue # Filter by status design_status = metadata.get("status", "unknown") if status and design_status != status: continue # Count products products = metadata.get("products", []) product_count = len(products) designs_found += 1 table.add_row( f"{category_dir.name}/{design_dir.name}", metadata.get("name", design_dir.name), design_status, str(product_count), metadata.get("created", "unknown"), ) if designs_found == 0: console.print("[yellow]No designs found.[/yellow]") console.print("Create a new design with: mycopunk design new stickers/my-design") else: console.print(table) console.print(f"\n[dim]Total: {designs_found} designs[/dim]") def validate_design(path: str, strict: bool = False) -> None: """Validate a design meets POD requirements.""" from PIL import Image design_dir = DESIGNS_DIR / path if not design_dir.exists(): console.print(f"[red]Error: Design not found at {design_dir}[/red]") raise SystemExit(1) metadata_path = design_dir / "metadata.yaml" if not metadata_path.exists(): console.print("[red]Error: metadata.yaml not found[/red]") raise SystemExit(1) with open(metadata_path) as f: metadata = yaml.safe_load(f) console.print(f"\n[bold]Validating: {metadata.get('name', path)}[/bold]\n") errors = [] warnings = [] passed = [] # Check required metadata fields required_fields = ["name", "slug", "source", "products", "status"] for field in required_fields: if field not in metadata: errors.append(f"Missing required field: {field}") else: passed.append(f"Metadata field '{field}' present") # Check source file exists source_info = metadata.get("source", {}) source_file = source_info.get("file") if source_file: source_path = design_dir / source_file if source_path.exists(): passed.append(f"Source file exists: {source_file}") # If PNG exists, check dimensions export_png = design_dir / "exports" / "300dpi" / source_file.replace(".svg", ".png") if export_png.exists(): try: with Image.open(export_png) as img: width, height = img.size expected_w = source_info.get("dimensions", {}).get("width") expected_h = source_info.get("dimensions", {}).get("height") if expected_w and expected_h: if width == expected_w and height == expected_h: passed.append(f"Dimensions correct: {width}×{height}") else: errors.append(f"Dimensions mismatch: got {width}×{height}, expected {expected_w}×{expected_h}") # Check DPI (approximate from metadata) if img.info.get("dpi"): dpi = img.info["dpi"][0] if dpi >= 300: passed.append(f"Resolution: {dpi} DPI") else: warnings.append(f"Low resolution: {dpi} DPI (recommended: 300+)") # Check for transparency if img.mode == "RGBA": passed.append("Transparency: Supported") else: warnings.append(f"No transparency: Image mode is {img.mode}") except Exception as e: warnings.append(f"Could not analyze PNG: {e}") else: warnings.append("No exported PNG found - run 'mycopunk design export' first") else: errors.append(f"Source file missing: {source_file}") else: errors.append("No source file specified in metadata") # Check products configuration products = metadata.get("products", []) if not products: warnings.append("No products configured") else: for i, product in enumerate(products): if "type" not in product: errors.append(f"Product {i+1}: Missing 'type' field") if "provider" not in product: errors.append(f"Product {i+1}: Missing 'provider' field") if "retail_price" not in product: warnings.append(f"Product {i+1}: No retail price set") passed.append(f"Products configured: {len(products)}") # Print results for msg in passed: console.print(f" [green]✓[/green] {msg}") for msg in warnings: console.print(f" [yellow]⚠[/yellow] {msg}") for msg in errors: console.print(f" [red]✗[/red] {msg}") console.print() if errors: console.print(f"[red]Validation failed with {len(errors)} error(s)[/red]") raise SystemExit(1) elif warnings and strict: console.print(f"[yellow]Validation failed with {len(warnings)} warning(s) (strict mode)[/yellow]") raise SystemExit(1) elif warnings: console.print(f"[yellow]Validation passed with {len(warnings)} warning(s)[/yellow]") else: console.print("[green]✓ Validation passed[/green]")