feat: Initialize mycopunk-swag repository

Phase 1 implementation:
- CLI scaffolding with Typer (design, product, mockup, catalog commands)
- Design management with templates (sticker, tshirt, print)
- Export automation via Inkscape CLI
- POD provider base classes (Printful, Prodigi)
- Product catalog configuration (products.yaml)
- Design guidelines and workflow documentation
- Helper scripts (batch-export, generate-mockups, sync-catalog)
- SVG templates for common product sizes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-01-24 19:26:58 +01:00
commit a937a0b978
31 changed files with 4295 additions and 0 deletions

70
.gitignore vendored Normal file
View File

@ -0,0 +1,70 @@
# Environment and secrets
config/.env
.env
*.env.local
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual environments
venv/
ENV/
env/
.venv/
# IDE
.idea/
.vscode/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Exports (regenerate with mycopunk design export)
designs/**/exports/300dpi/*.png
designs/**/exports/300dpi/*.jpg
designs/**/exports/mockups/*.png
designs/**/exports/mockups/*.jpg
# Keep .gitkeep files
!.gitkeep
# Cache
.cache/
*.cache
# Logs
*.log
logs/
# Test coverage
.coverage
htmlcov/
.pytest_cache/
.mypy_cache/
# Build artifacts
*.tar.gz
*.zip

123
CLAUDE.md Normal file
View File

@ -0,0 +1,123 @@
# Mycopunk Swag - AI Assistant Context
## Project Overview
This repository manages mycopunk merchandise (stickers, shirts, patches) with automated print-on-demand fulfillment. The workflow is CLI-driven, using open-source tools (Inkscape, GIMP, ImageMagick) for design work and API integrations with Printful and Prodigi for fulfillment.
## Key Concepts
### Design Structure
Each design lives in its own directory under `designs/{category}/{slug}/`:
- `{slug}.svg` - Source vector file
- `metadata.yaml` - Product configuration and metadata
- `exports/` - Generated output files (don't edit manually)
### POD Providers
- **Printful** - Primary for apparel (t-shirts, hoodies)
- **Prodigi** - Primary for stickers and art prints
### CLI Tool
Located in `cli/`, installed with `pip install -e .`
Main entry point: `mycopunk` command
## File Locations
| What | Where |
|------|-------|
| Design sources | `designs/{stickers,shirts,misc}/` |
| Product templates | `templates/{printful,prodigi}/` |
| CLI source | `cli/mycopunk/` |
| Configuration | `config/` |
| API credentials | `config/.env` (gitignored) |
## Common Tasks
### Creating a New Design
```bash
mycopunk design new stickers/design-name --template=sticker-3x3
# Then edit designs/stickers/design-name/design-name.svg in Inkscape
```
### Exporting Designs
```bash
# Export single design
mycopunk design export stickers/design-name
# Batch export all stickers
mycopunk batch export --type=sticker
```
### POD Operations
```bash
# Always test in sandbox first
mycopunk product create stickers/design-name --provider=prodigi --sandbox
# Push to production
mycopunk product push stickers/design-name --provider=prodigi
```
## Design Requirements Quick Reference
| Product | Dimensions | Pixels @ 300 DPI |
|---------|-----------|------------------|
| T-shirt front | 12" × 16" | 3600 × 4800 |
| Sticker 3×3" | 3" × 3" | 900 × 900 |
| Sticker 6×6" | 6" × 6" | 1800 × 1800 |
| Art print 8×10 | 8" × 10" | 2400 × 3000 |
## Metadata.yaml Structure
```yaml
name: "Design Name"
slug: design-name
description: "What this design represents"
tags: [mycelium, punk, nature]
created: 2025-01-24
author: jeff
source:
file: design-name.svg
format: svg
dimensions: { width: 4800, height: 4800 }
dpi: 300
color_profile: sRGB
products:
- type: sticker
provider: prodigi
sku: STICKER-VINYL-KISS-3X3
variants: [matte, gloss]
retail_price: 3.50
status: active # draft, active, retired
```
## API Configuration
Credentials stored in `config/.env` (never commit):
```bash
PRINTFUL_API_TOKEN=xxx
PRODIGI_API_KEY_SANDBOX=xxx
PRODIGI_API_KEY_LIVE=xxx
```
## Development Workflow
1. Work on `dev` branch for new features
2. Test CLI changes with `pip install -e .`
3. Run `mycopunk design validate` before committing
4. Push to Gitea, auto-mirrors to GitHub
5. Merge to `main` when verified working
## Important Notes
- Always validate designs before pushing to POD services
- Use `--sandbox` flag when testing POD integrations
- Exports are gitignored - regenerate with `mycopunk design export`
- Mockups require POD API access (not available offline)
## Links
- [Printful API Docs](https://developers.printful.com/docs/)
- [Prodigi API Docs](https://www.prodigi.com/print-api/docs/)
- [Inkscape CLI Reference](https://wiki.inkscape.org/wiki/Using_the_Command_Line)

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Jeff Emmett
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

109
README.md Normal file
View File

@ -0,0 +1,109 @@
# 🍄 Mycopunk Swag
**Decentralized merchandise for the mycelial revolution.**
A CLI-driven workflow for designing, managing, and distributing mycopunk merchandise (stickers, shirts, patches, zines) with automated print-on-demand fulfillment.
## Philosophy
Like mycelium networks that share resources without central control, this project embodies:
- **Open source designs** - Fork, remix, distribute
- **Decentralized production** - POD means no inventory, no warehouses
- **Anarchist tech** - Self-hosted, privacy-respecting, no platform lock-in
- **Community-driven** - Contribute designs, suggest products, share the spores
## Quick Start
```bash
# Install CLI
cd cli && pip install -e .
# Create a new design
mycopunk design new stickers/my-design --template=sticker-3x3
# Export to print-ready files
mycopunk design export stickers/my-design
# Validate against POD requirements
mycopunk design validate stickers/my-design
# Create product on POD service
mycopunk product create stickers/my-design --provider=prodigi
```
## Project Structure
```
mycopunk-swag/
├── designs/ # Source design files (SVG, XCF)
│ ├── stickers/ # Sticker designs
│ ├── shirts/ # Apparel designs
│ └── misc/ # Patches, pins, zines
├── templates/ # POD service templates
├── cli/ # Python CLI tool
├── config/ # Product & pricing configuration
├── scripts/ # Helper scripts
└── docs/ # Documentation
```
## Design Workflow
1. **Create** - Design in Inkscape/GIMP, save as SVG
2. **Configure** - Add `metadata.yaml` with product mappings
3. **Export** - Generate print-ready PNGs at required DPIs
4. **Validate** - Check against POD service requirements
5. **Push** - Upload to Printful/Prodigi
6. **Mockup** - Generate product mockups for shop
## Print-on-Demand Providers
| Provider | Products | Best For |
|----------|----------|----------|
| **Printful** | Apparel, accessories | T-shirts, hoodies, quality |
| **Prodigi** | Stickers, prints | Vinyl stickers, art prints, global shipping |
## CLI Reference
See [docs/cli-reference.md](docs/cli-reference.md) for full command documentation.
### Core Commands
```bash
mycopunk design new <path> # Create new design scaffold
mycopunk design list # List all designs
mycopunk design export <path> # Export to print formats
mycopunk design validate <path> # Validate requirements
mycopunk product create <path> # Create POD product
mycopunk product push <path> # Push design to POD
mycopunk product list # List all products
mycopunk mockup generate <path> # Generate mockups
mycopunk catalog sync # Sync POD catalogs
```
## Contributing Designs
1. Fork the repo
2. Create your design in `designs/` following the structure
3. Add `metadata.yaml` with required fields
4. Run `mycopunk design validate` to check requirements
5. Submit a PR with mockups in the description
### Design Requirements
- **Format**: SVG (vectors) or high-res PNG (4800×4800 minimum)
- **Resolution**: 300 DPI for all exports
- **Color Profile**: sRGB
- **Transparency**: PNG with transparent background preferred
See [docs/design-guidelines.md](docs/design-guidelines.md) for detailed specs.
## License
Designs are released under [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) unless otherwise noted.
Code is released under [MIT License](LICENSE).
---
*Spread the spores. Decentralize everything.* 🍄

25
backlog/config.yml Normal file
View File

@ -0,0 +1,25 @@
project_name: "Mycopunk Swag"
default_status: "To Do"
statuses: ["To Do", "In Progress", "Done"]
labels: [cli, pod-integration, design, documentation, infrastructure]
milestones:
- name: "Phase 1: Foundation"
description: "Repository setup, CLI scaffolding, documentation"
- name: "Phase 2: CLI Core"
description: "Design management, export automation"
- name: "Phase 3: POD Integration"
description: "Printful and Prodigi API wrappers"
- name: "Phase 4: Automation"
description: "Batch operations, CI/CD, monitoring"
- name: "Phase 5: Custom Shop"
description: "Headless commerce, storefront, payments"
date_format: yyyy-mm-dd
max_column_width: 20
auto_open_browser: true
default_port: 6420
remote_operations: true
auto_commit: false
zero_padded_ids: 3
bypass_git_hooks: false
check_active_branches: true
active_branch_days: 60

81
cli/README.md Normal file
View File

@ -0,0 +1,81 @@
# Mycopunk CLI
Command-line tool for managing mycopunk merchandise and print-on-demand fulfillment.
## Installation
```bash
# Install in development mode
pip install -e .
# Or install with dev dependencies
pip install -e ".[dev]"
```
## Quick Start
```bash
# Create a new sticker design
mycopunk design new stickers/my-design --template=sticker-3x3
# Export to print-ready PNG
mycopunk design export stickers/my-design
# Validate against POD requirements
mycopunk design validate stickers/my-design
# Create product on Prodigi (sandbox mode)
mycopunk product create stickers/my-design --provider=prodigi --sandbox
```
## Commands
See the full [CLI Reference](../docs/cli-reference.md) for all commands.
### Design Management
- `mycopunk design new` - Create design scaffold
- `mycopunk design list` - List all designs
- `mycopunk design export` - Export to print formats
- `mycopunk design validate` - Validate requirements
### POD Integration
- `mycopunk product create` - Create product on POD
- `mycopunk product push` - Push design updates
- `mycopunk product list` - List products
- `mycopunk mockup generate` - Generate mockups
### Batch Operations
- `mycopunk batch export` - Export all designs
- `mycopunk batch push` - Push all to POD
## Configuration
Copy `../config/.env.example` to `../config/.env` and add your API keys:
```bash
PRINTFUL_API_TOKEN=your_token
PRODIGI_API_KEY_SANDBOX=your_sandbox_key
PRODIGI_API_KEY_LIVE=your_live_key
```
## Development
```bash
# Run tests
pytest
# Format code
black mycopunk/
# Lint
ruff check mycopunk/
# Type check
mypy mycopunk/
```
## Requirements
- Python 3.10+
- Inkscape (for SVG export)
- ImageMagick (optional, for local mockups)

9
cli/mycopunk/__init__.py Normal file
View File

@ -0,0 +1,9 @@
"""
Mycopunk CLI - Merchandise management and POD fulfillment tool.
A CLI-driven workflow for designing, managing, and distributing
mycopunk merchandise with automated print-on-demand fulfillment.
"""
__version__ = "0.1.0"
__author__ = "Jeff Emmett"

199
cli/mycopunk/catalog.py Normal file
View File

@ -0,0 +1,199 @@
"""
Catalog and product management for POD services.
"""
from pathlib import Path
from typing import Optional
import yaml
from rich.console import Console
from rich.table import Table
console = Console()
def get_project_root() -> Path:
"""Find the project root."""
current = Path.cwd()
while current != current.parent:
if (current / "designs").is_dir():
return current
current = current.parent
return Path.cwd()
DESIGNS_DIR = get_project_root() / "designs"
CONFIG_DIR = get_project_root() / "config"
def create_product(path: str, provider: str, sandbox: bool = False) -> None:
"""Create a product on a POD service."""
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)
products = metadata.get("products", [])
matching_products = [p for p in products if p.get("provider") == provider]
if not matching_products:
console.print(f"[yellow]No products configured for provider '{provider}'[/yellow]")
console.print("Add products to metadata.yaml first.")
raise SystemExit(1)
mode = "SANDBOX" if sandbox else "PRODUCTION"
console.print(f"\n[bold]Creating products on {provider} ({mode})[/bold]\n")
for product in matching_products:
product_type = product.get("type", "unknown")
sku = product.get("sku", "N/A")
price = product.get("retail_price", 0)
console.print(f" Product: {product_type}")
console.print(f" SKU: {sku}")
console.print(f" Price: ${price:.2f}")
if provider == "printful":
_create_printful_product(design_dir, metadata, product, sandbox)
elif provider == "prodigi":
_create_prodigi_product(design_dir, metadata, product, sandbox)
else:
console.print(f"[red] Unknown provider: {provider}[/red]")
console.print()
def _create_printful_product(
design_dir: Path,
metadata: dict,
product: dict,
sandbox: bool
) -> None:
"""Create product on Printful."""
console.print("[yellow] → Printful API integration not yet implemented[/yellow]")
console.print("[dim] Would create product with Printful API[/dim]")
# TODO: Implement Printful API call
# 1. Upload design file to Printful
# 2. Create sync product
# 3. Add sync variants
# 4. Store Printful product ID in metadata
def _create_prodigi_product(
design_dir: Path,
metadata: dict,
product: dict,
sandbox: bool
) -> None:
"""Create product on Prodigi."""
console.print("[yellow] → Prodigi API integration not yet implemented[/yellow]")
console.print("[dim] Would create product with Prodigi API[/dim]")
# TODO: Implement Prodigi API call
# 1. Upload design file to Prodigi
# 2. Create product template
# 3. Store Prodigi template ID in metadata
def push_product(path: str, provider: Optional[str] = None) -> None:
"""Push design updates to POD product."""
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)
products = metadata.get("products", [])
if provider:
products = [p for p in products if p.get("provider") == provider]
if not products:
console.print("[yellow]No products to push[/yellow]")
raise SystemExit(1)
console.print(f"\n[bold]Pushing updates for: {metadata.get('name', path)}[/bold]\n")
for product in products:
prod_provider = product.get("provider", "unknown")
product_type = product.get("type", "unknown")
console.print(f" Pushing {product_type} to {prod_provider}...")
console.print("[yellow] → API integration not yet implemented[/yellow]")
# TODO: Implement push logic
# 1. Check if product exists on provider
# 2. Upload updated design file
# 3. Sync any metadata changes
def list_products(provider: Optional[str] = None) -> None:
"""List all products across designs."""
table = Table(title="🍄 Mycopunk Products")
table.add_column("Design", style="cyan")
table.add_column("Type", style="green")
table.add_column("Provider", style="yellow")
table.add_column("SKU", style="blue")
table.add_column("Price", style="magenta", justify="right")
table.add_column("Status", style="dim")
products_found = 0
for category_dir in DESIGNS_DIR.iterdir():
if not category_dir.is_dir():
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
design_path = f"{category_dir.name}/{design_dir.name}"
design_status = metadata.get("status", "unknown")
products = metadata.get("products", [])
for product in products:
prod_provider = product.get("provider", "unknown")
if provider and prod_provider != provider:
continue
products_found += 1
table.add_row(
design_path,
product.get("type", "unknown"),
prod_provider,
product.get("sku", "N/A"),
f"${product.get('retail_price', 0):.2f}",
design_status,
)
if products_found == 0:
console.print("[yellow]No products found.[/yellow]")
else:
console.print(table)
console.print(f"\n[dim]Total: {products_found} product(s)[/dim]")

213
cli/mycopunk/cli.py Normal file
View File

@ -0,0 +1,213 @@
"""
Main CLI entry point for mycopunk.
"""
import typer
from rich.console import Console
from rich.table import Table
from typing import Optional
from pathlib import Path
from . import __version__
app = typer.Typer(
name="mycopunk",
help="🍄 CLI tool for managing mycopunk merchandise and POD fulfillment",
add_completion=True,
)
console = Console()
# Sub-command groups
design_app = typer.Typer(help="Design management commands")
product_app = typer.Typer(help="POD product commands")
mockup_app = typer.Typer(help="Mockup generation commands")
catalog_app = typer.Typer(help="Catalog management commands")
batch_app = typer.Typer(help="Batch operations")
app.add_typer(design_app, name="design")
app.add_typer(product_app, name="product")
app.add_typer(mockup_app, name="mockup")
app.add_typer(catalog_app, name="catalog")
app.add_typer(batch_app, name="batch")
def version_callback(value: bool) -> None:
if value:
console.print(f"mycopunk version {__version__}")
raise typer.Exit()
@app.callback()
def main(
version: Optional[bool] = typer.Option(
None, "--version", "-v", callback=version_callback, is_eager=True,
help="Show version and exit"
),
) -> None:
"""
🍄 Mycopunk CLI - Merchandise management and POD fulfillment.
Manage designs, generate exports, and push products to print-on-demand
services like Printful and Prodigi.
"""
pass
# ============================================
# Design Commands
# ============================================
@design_app.command("new")
def design_new(
path: str = typer.Argument(..., help="Design path (e.g., stickers/my-design)"),
template: str = typer.Option(
"sticker-3x3", "--template", "-t",
help="Template to use (sticker-3x3, tshirt-front, etc.)"
),
) -> None:
"""Create a new design scaffold with metadata template."""
from .design import create_design
create_design(path, template)
@design_app.command("list")
def design_list(
status: Optional[str] = typer.Option(None, "--status", "-s", help="Filter by status"),
design_type: Optional[str] = typer.Option(None, "--type", "-t", help="Filter by type"),
) -> None:
"""List all designs in the repository."""
from .design import list_designs
list_designs(status, design_type)
@design_app.command("export")
def design_export(
path: str = typer.Argument(..., help="Design path to export"),
dpi: int = typer.Option(300, "--dpi", help="Export resolution"),
format: str = typer.Option("png", "--format", "-f", help="Export format (png, jpg)"),
) -> None:
"""Export design to print-ready files."""
from .export import export_design
export_design(path, dpi, format)
@design_app.command("validate")
def design_validate(
path: str = typer.Argument(..., help="Design path to validate"),
strict: bool = typer.Option(False, "--strict", help="Fail on warnings"),
) -> None:
"""Validate design meets POD requirements."""
from .design import validate_design
validate_design(path, strict)
# ============================================
# Product Commands
# ============================================
@product_app.command("create")
def product_create(
path: str = typer.Argument(..., help="Design path"),
provider: str = typer.Option("prodigi", "--provider", "-p", help="POD provider"),
sandbox: bool = typer.Option(False, "--sandbox", help="Use sandbox/test mode"),
) -> None:
"""Create product on POD service from design."""
from .catalog import create_product
create_product(path, provider, sandbox)
@product_app.command("push")
def product_push(
path: str = typer.Argument(..., help="Design path"),
provider: Optional[str] = typer.Option(None, "--provider", "-p", help="POD provider"),
) -> None:
"""Push design updates to POD product."""
from .catalog import push_product
push_product(path, provider)
@product_app.command("list")
def product_list(
provider: Optional[str] = typer.Option(None, "--provider", "-p", help="Filter by provider"),
) -> None:
"""List all products by provider."""
from .catalog import list_products
list_products(provider)
# ============================================
# Mockup Commands
# ============================================
@mockup_app.command("generate")
def mockup_generate(
path: str = typer.Argument(..., help="Design path"),
all_variants: bool = typer.Option(False, "--all", help="Generate all variants"),
color: Optional[str] = typer.Option(None, "--color", "-c", help="Product color"),
size: Optional[str] = typer.Option(None, "--size", "-s", help="Product size"),
) -> None:
"""Generate product mockups for a design."""
console.print(f"[yellow]Generating mockups for {path}...[/yellow]")
console.print("[dim]Mockup generation requires POD API access.[/dim]")
# TODO: Implement mockup generation
console.print("[red]Not yet implemented[/red]")
# ============================================
# Catalog Commands
# ============================================
@catalog_app.command("sync")
def catalog_sync(
provider: str = typer.Option("all", "--provider", "-p", help="Provider to sync"),
) -> None:
"""Sync product catalog from POD services."""
console.print(f"[yellow]Syncing catalog from {provider}...[/yellow]")
# TODO: Implement catalog sync
console.print("[red]Not yet implemented[/red]")
# ============================================
# Batch Commands
# ============================================
@batch_app.command("export")
def batch_export(
design_type: Optional[str] = typer.Option(None, "--type", "-t", help="Filter by type"),
status: str = typer.Option("active", "--status", "-s", help="Filter by status"),
) -> None:
"""Export all designs matching criteria."""
console.print(f"[yellow]Batch exporting designs (type={design_type}, status={status})...[/yellow]")
# TODO: Implement batch export
console.print("[red]Not yet implemented[/red]")
@batch_app.command("push")
def batch_push(
status: str = typer.Option("active", "--status", "-s", help="Filter by status"),
provider: Optional[str] = typer.Option(None, "--provider", "-p", help="POD provider"),
) -> None:
"""Push all designs to POD services."""
console.print(f"[yellow]Batch pushing designs (status={status})...[/yellow]")
# TODO: Implement batch push
console.print("[red]Not yet implemented[/red]")
# ============================================
# Report Commands
# ============================================
@app.command("report")
def report(
report_type: str = typer.Argument("pricing", help="Report type (pricing, inventory, orders)"),
output: Optional[Path] = typer.Option(None, "--output", "-o", help="Output file"),
) -> None:
"""Generate reports (pricing, inventory, orders)."""
console.print(f"[yellow]Generating {report_type} report...[/yellow]")
# TODO: Implement reports
console.print("[red]Not yet implemented[/red]")
if __name__ == "__main__":
app()

172
cli/mycopunk/config.py Normal file
View File

@ -0,0 +1,172 @@
"""
Configuration management for mycopunk CLI.
"""
import os
from pathlib import Path
from typing import Optional, Any
import yaml
from dotenv import load_dotenv
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
return Path.cwd()
PROJECT_ROOT = get_project_root()
CONFIG_DIR = PROJECT_ROOT / "config"
class Config:
"""Configuration manager for mycopunk."""
_instance: Optional["Config"] = None
def __new__(cls) -> "Config":
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self) -> None:
if self._initialized:
return
self._initialized = True
self._products: dict = {}
self._pricing: dict = {}
# Load environment variables
env_path = CONFIG_DIR / ".env"
if env_path.exists():
load_dotenv(env_path)
# Load product configuration
products_path = CONFIG_DIR / "products.yaml"
if products_path.exists():
with open(products_path) as f:
self._products = yaml.safe_load(f) or {}
# Load pricing configuration
pricing_path = CONFIG_DIR / "pricing.yaml"
if pricing_path.exists():
with open(pricing_path) as f:
self._pricing = yaml.safe_load(f) or {}
@property
def env(self) -> str:
"""Get current environment (development/production)."""
return os.getenv("MYCOPUNK_ENV", "development")
@property
def is_production(self) -> bool:
"""Check if running in production mode."""
return self.env == "production"
@property
def debug(self) -> bool:
"""Check if debug mode is enabled."""
return os.getenv("DEBUG", "false").lower() == "true"
@property
def default_provider(self) -> str:
"""Get default POD provider."""
return os.getenv("DEFAULT_PROVIDER", "prodigi")
# API Keys
@property
def printful_token(self) -> Optional[str]:
"""Get Printful API token."""
return os.getenv("PRINTFUL_API_TOKEN")
@property
def prodigi_key(self) -> Optional[str]:
"""Get Prodigi API key (sandbox or live based on env)."""
if self.is_production:
return os.getenv("PRODIGI_API_KEY_LIVE")
return os.getenv("PRODIGI_API_KEY_SANDBOX")
@property
def stripe_key(self) -> Optional[str]:
"""Get Stripe secret key."""
return os.getenv("STRIPE_SECRET_KEY")
# Product Configuration
def get_product_config(self, product_type: str, size: str = "small") -> dict:
"""Get product configuration by type and size."""
type_config = self._products.get(product_type, {})
return type_config.get(size, {})
def get_provider_sku(
self,
product_type: str,
size: str,
provider: str
) -> Optional[str]:
"""Get provider-specific SKU for a product."""
config = self.get_product_config(product_type, size)
providers = config.get("providers", {})
provider_config = providers.get(provider, {})
return provider_config.get("sku")
def get_base_cost(
self,
product_type: str,
size: str,
provider: str,
variant: Optional[str] = None
) -> float:
"""Get base cost for a product from provider."""
config = self.get_product_config(product_type, size)
providers = config.get("providers", {})
provider_config = providers.get(provider, {})
base_cost = provider_config.get("base_cost", 0)
# Handle size-specific pricing (apparel)
if isinstance(base_cost, dict) and variant:
return base_cost.get(variant, 0)
return float(base_cost) if not isinstance(base_cost, dict) else 0
# Pricing
def calculate_retail_price(
self,
base_cost: float,
product_type: str
) -> float:
"""Calculate retail price from base cost with markup."""
pricing = self._pricing or self._products.get("pricing", {})
rules = pricing.get("rules", {})
type_rules = rules.get(product_type, {})
markup = type_rules.get("markup", pricing.get("default_markup", 2.0))
minimum = type_rules.get("minimum_price", 0)
calculated = base_cost * markup
price = max(calculated, minimum)
# Apply rounding
rounding = pricing.get("rounding", "nearest_99")
if rounding == "nearest_99":
price = round(price) - 0.01
elif rounding == "nearest_50":
price = round(price * 2) / 2
return max(price, minimum)
# Singleton accessor
def get_config() -> Config:
"""Get the configuration singleton."""
return Config()

383
cli/mycopunk/design.py Normal file
View File

@ -0,0 +1,383 @@
"""
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'''<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg"
width="{template_config["width"]}"
height="{template_config["height"]}"
viewBox="0 0 {template_config["width"]} {template_config["height"]}">
<!--
Mycopunk Design: {name}
Template: {template}
Canvas: {template_config["width"]}x{template_config["height"]} px @ 300 DPI
Replace this content with your design.
-->
<rect width="100%" height="100%" fill="none" stroke="#ccc" stroke-width="2" stroke-dasharray="10,10"/>
<text x="50%" y="50%" text-anchor="middle" fill="#999" font-family="sans-serif" font-size="48">
{name}
</text>
</svg>'''
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]")

268
cli/mycopunk/export.py Normal file
View File

@ -0,0 +1,268 @@
"""
Export automation for mycopunk designs.
Uses Inkscape CLI for SVG to PNG conversion.
"""
import subprocess
import shutil
from pathlib import Path
from typing import Optional
import yaml
from rich.console import Console
from rich.progress import Progress, SpinnerColumn, TextColumn
console = Console()
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
return Path.cwd()
DESIGNS_DIR = get_project_root() / "designs"
def check_inkscape() -> bool:
"""Check if Inkscape is available."""
return shutil.which("inkscape") is not None
def export_design(path: str, dpi: int = 300, format: str = "png") -> None:
"""Export a design to print-ready files."""
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)
source_info = metadata.get("source", {})
source_file = source_info.get("file")
if not source_file:
console.print("[red]Error: No source file specified in metadata[/red]")
raise SystemExit(1)
source_path = design_dir / source_file
if not source_path.exists():
console.print(f"[red]Error: Source file not found: {source_path}[/red]")
raise SystemExit(1)
# Determine export method based on file type
if source_file.endswith(".svg"):
export_svg(source_path, design_dir, metadata, dpi, format)
elif source_file.endswith((".png", ".jpg", ".jpeg")):
export_raster(source_path, design_dir, metadata, dpi, format)
else:
console.print(f"[red]Error: Unsupported source format: {source_file}[/red]")
raise SystemExit(1)
def export_svg(
source_path: Path,
design_dir: Path,
metadata: dict,
dpi: int,
format: str
) -> None:
"""Export SVG using Inkscape."""
if not check_inkscape():
console.print("[red]Error: Inkscape not found. Please install Inkscape.[/red]")
console.print("[dim]Ubuntu: sudo apt install inkscape[/dim]")
raise SystemExit(1)
source_info = metadata.get("source", {})
slug = source_path.stem
# Create export directory
export_dir = design_dir / "exports" / f"{dpi}dpi"
export_dir.mkdir(parents=True, exist_ok=True)
output_file = export_dir / f"{slug}.{format}"
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
console=console,
) as progress:
task = progress.add_task(f"Exporting {slug} at {dpi} DPI...", total=None)
# Build Inkscape command
cmd = [
"inkscape",
str(source_path),
f"--export-type={format}",
f"--export-dpi={dpi}",
f"--export-filename={output_file}",
]
# Add transparent background for PNG
if format == "png":
cmd.append("--export-background-opacity=0")
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=120, # 2 minute timeout
)
if result.returncode != 0:
progress.stop()
console.print(f"[red]Inkscape error:[/red]\n{result.stderr}")
raise SystemExit(1)
progress.update(task, description="Export complete!")
except subprocess.TimeoutExpired:
progress.stop()
console.print("[red]Error: Export timed out[/red]")
raise SystemExit(1)
except FileNotFoundError:
progress.stop()
console.print("[red]Error: Inkscape not found[/red]")
raise SystemExit(1)
# Verify output
if output_file.exists():
size_kb = output_file.stat().st_size / 1024
console.print(f"\n[green]✓ Exported:[/green] {output_file}")
console.print(f"[dim] Size: {size_kb:.1f} KB[/dim]")
# Try to get dimensions using PIL
try:
from PIL import Image
with Image.open(output_file) as img:
width, height = img.size
console.print(f"[dim] Dimensions: {width}×{height} px[/dim]")
except Exception:
pass
else:
console.print("[red]Error: Export failed - output file not created[/red]")
raise SystemExit(1)
def export_raster(
source_path: Path,
design_dir: Path,
metadata: dict,
dpi: int,
format: str
) -> None:
"""Export/convert raster images using PIL."""
from PIL import Image
source_info = metadata.get("source", {})
slug = source_path.stem
# Create export directory
export_dir = design_dir / "exports" / f"{dpi}dpi"
export_dir.mkdir(parents=True, exist_ok=True)
output_file = export_dir / f"{slug}.{format}"
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
console=console,
) as progress:
task = progress.add_task(f"Processing {slug}...", total=None)
try:
with Image.open(source_path) as img:
# Convert to appropriate mode
if format == "png" and img.mode != "RGBA":
img = img.convert("RGBA")
elif format in ("jpg", "jpeg") and img.mode == "RGBA":
# Flatten transparency for JPEG
background = Image.new("RGB", img.size, (255, 255, 255))
background.paste(img, mask=img.split()[3])
img = background
# Save with DPI metadata
img.save(output_file, dpi=(dpi, dpi))
except Exception as e:
progress.stop()
console.print(f"[red]Error processing image: {e}[/red]")
raise SystemExit(1)
progress.update(task, description="Processing complete!")
# Verify output
if output_file.exists():
size_kb = output_file.stat().st_size / 1024
console.print(f"\n[green]✓ Exported:[/green] {output_file}")
console.print(f"[dim] Size: {size_kb:.1f} KB[/dim]")
with Image.open(output_file) as img:
width, height = img.size
console.print(f"[dim] Dimensions: {width}×{height} px[/dim]")
else:
console.print("[red]Error: Export failed[/red]")
raise SystemExit(1)
def batch_export(
design_type: Optional[str] = None,
status: str = "active",
dpi: int = 300,
format: str = "png"
) -> None:
"""Export all designs matching criteria."""
designs_exported = 0
errors = []
for category_dir in DESIGNS_DIR.iterdir():
if not category_dir.is_dir():
continue
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
design_status = metadata.get("status", "unknown")
if design_status != status:
continue
path = f"{category_dir.name}/{design_dir.name}"
console.print(f"\n[bold]Exporting: {path}[/bold]")
try:
export_design(path, dpi, format)
designs_exported += 1
except SystemExit:
errors.append(path)
console.print(f"\n[green]Exported {designs_exported} design(s)[/green]")
if errors:
console.print(f"[red]Failed: {len(errors)} design(s)[/red]")
for e in errors:
console.print(f" [red]• {e}[/red]")

View File

@ -0,0 +1,9 @@
"""
Print-on-demand provider integrations.
"""
from .base import PODProvider
from .printful import PrintfulProvider
from .prodigi import ProdigiProvider
__all__ = ["PODProvider", "PrintfulProvider", "ProdigiProvider"]

152
cli/mycopunk/pod/base.py Normal file
View File

@ -0,0 +1,152 @@
"""
Base class for POD provider integrations.
"""
from abc import ABC, abstractmethod
from pathlib import Path
from typing import Optional, Any
from dataclasses import dataclass
@dataclass
class ProductResult:
"""Result of a product creation/update operation."""
success: bool
product_id: Optional[str] = None
error: Optional[str] = None
data: Optional[dict] = None
@dataclass
class MockupResult:
"""Result of a mockup generation operation."""
success: bool
image_url: Optional[str] = None
local_path: Optional[Path] = None
error: Optional[str] = None
class PODProvider(ABC):
"""Abstract base class for print-on-demand providers."""
name: str = "base"
def __init__(self, api_key: str, sandbox: bool = False) -> None:
self.api_key = api_key
self.sandbox = sandbox
self._validate_credentials()
@abstractmethod
def _validate_credentials(self) -> None:
"""Validate API credentials."""
pass
@abstractmethod
async def create_product(
self,
design_path: Path,
metadata: dict,
product_config: dict,
) -> ProductResult:
"""
Create a new product on the POD service.
Args:
design_path: Path to the design file (PNG)
metadata: Design metadata from metadata.yaml
product_config: Product configuration from products.yaml
Returns:
ProductResult with product_id if successful
"""
pass
@abstractmethod
async def update_product(
self,
product_id: str,
design_path: Path,
metadata: dict,
) -> ProductResult:
"""
Update an existing product.
Args:
product_id: Provider's product ID
design_path: Path to updated design file
metadata: Updated metadata
Returns:
ProductResult indicating success/failure
"""
pass
@abstractmethod
async def generate_mockup(
self,
product_id: str,
variant: Optional[str] = None,
) -> MockupResult:
"""
Generate a product mockup.
Args:
product_id: Provider's product ID
variant: Optional variant (color, size) for mockup
Returns:
MockupResult with image URL or local path
"""
pass
@abstractmethod
async def get_product(self, product_id: str) -> Optional[dict]:
"""
Get product details from the provider.
Args:
product_id: Provider's product ID
Returns:
Product data dict or None if not found
"""
pass
@abstractmethod
async def list_products(self, limit: int = 100) -> list[dict]:
"""
List products from the provider.
Args:
limit: Maximum number of products to return
Returns:
List of product data dicts
"""
pass
@abstractmethod
async def get_catalog(self, product_type: Optional[str] = None) -> list[dict]:
"""
Get available products from provider catalog.
Args:
product_type: Optional filter by type (sticker, tshirt, etc)
Returns:
List of catalog items
"""
pass
@abstractmethod
async def upload_file(self, file_path: Path) -> Optional[str]:
"""
Upload a file to the provider.
Args:
file_path: Local path to file
Returns:
File URL or ID from provider, or None on failure
"""
pass

View File

@ -0,0 +1,325 @@
"""
Printful API integration.
API Documentation: https://developers.printful.com/docs/
"""
from pathlib import Path
from typing import Optional
import httpx
from .base import PODProvider, ProductResult, MockupResult
class PrintfulProvider(PODProvider):
"""Printful print-on-demand provider."""
name = "printful"
BASE_URL = "https://api.printful.com"
def __init__(self, api_key: str, sandbox: bool = False) -> None:
super().__init__(api_key, sandbox)
self.client = httpx.AsyncClient(
base_url=self.BASE_URL,
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
},
timeout=30.0,
)
def _validate_credentials(self) -> None:
"""Validate Printful API token."""
if not self.api_key:
raise ValueError("Printful API token is required")
# Note: Printful doesn't have a separate sandbox environment
# Test mode is controlled per-store
async def create_product(
self,
design_path: Path,
metadata: dict,
product_config: dict,
) -> ProductResult:
"""Create a sync product on Printful."""
try:
# 1. Upload the design file
file_url = await self.upload_file(design_path)
if not file_url:
return ProductResult(
success=False,
error="Failed to upload design file"
)
# 2. Build sync product payload
sku = product_config.get("sku")
colors = product_config.get("colors", ["black"])
sizes = product_config.get("sizes", ["M"])
placement = product_config.get("placement", "front")
# Build sync variants
sync_variants = []
for color in colors:
for size in sizes:
sync_variants.append({
"variant_id": self._get_variant_id(sku, color, size),
"files": [
{
"url": file_url,
"type": placement,
}
],
"retail_price": str(product_config.get("retail_price", "25.00")),
})
payload = {
"sync_product": {
"name": metadata.get("name", "Mycopunk Design"),
"thumbnail": file_url,
},
"sync_variants": sync_variants,
}
# 3. Create the product
response = await self.client.post("/store/products", json=payload)
response.raise_for_status()
data = response.json()
product_id = str(data.get("result", {}).get("id"))
return ProductResult(
success=True,
product_id=product_id,
data=data.get("result"),
)
except httpx.HTTPStatusError as e:
return ProductResult(
success=False,
error=f"HTTP {e.response.status_code}: {e.response.text}"
)
except Exception as e:
return ProductResult(success=False, error=str(e))
async def update_product(
self,
product_id: str,
design_path: Path,
metadata: dict,
) -> ProductResult:
"""Update an existing sync product."""
try:
# Upload new design
file_url = await self.upload_file(design_path)
if not file_url:
return ProductResult(
success=False,
error="Failed to upload design file"
)
# Get current product to update variants
current = await self.get_product(product_id)
if not current:
return ProductResult(
success=False,
error=f"Product {product_id} not found"
)
# Update product name/thumbnail
payload = {
"sync_product": {
"name": metadata.get("name"),
"thumbnail": file_url,
}
}
response = await self.client.put(
f"/store/products/{product_id}",
json=payload
)
response.raise_for_status()
# Update each variant's files
for variant in current.get("sync_variants", []):
variant_id = variant.get("id")
await self.client.put(
f"/store/variants/{variant_id}",
json={
"files": [{"url": file_url, "type": "front"}]
}
)
return ProductResult(
success=True,
product_id=product_id,
)
except Exception as e:
return ProductResult(success=False, error=str(e))
async def generate_mockup(
self,
product_id: str,
variant: Optional[str] = None,
) -> MockupResult:
"""Generate product mockup using Printful's mockup generator."""
try:
# Get product info
product = await self.get_product(product_id)
if not product:
return MockupResult(
success=False,
error=f"Product {product_id} not found"
)
# Get first variant or specified variant
variants = product.get("sync_variants", [])
if not variants:
return MockupResult(
success=False,
error="No variants found"
)
target_variant = variants[0]
if variant:
for v in variants:
if variant in str(v.get("name", "")).lower():
target_variant = v
break
# Request mockup generation
variant_id = target_variant.get("variant_id")
files = target_variant.get("files", [])
if not files:
return MockupResult(
success=False,
error="No files attached to variant"
)
payload = {
"variant_ids": [variant_id],
"files": [{"placement": f.get("type"), "image_url": f.get("url")} for f in files],
}
response = await self.client.post(
"/mockup-generator/create-task/variant_ids",
json=payload
)
response.raise_for_status()
data = response.json()
task_key = data.get("result", {}).get("task_key")
# Poll for result (simplified - in production, use proper polling)
import asyncio
for _ in range(10):
await asyncio.sleep(2)
status_response = await self.client.get(
f"/mockup-generator/task?task_key={task_key}"
)
status_data = status_response.json()
if status_data.get("result", {}).get("status") == "completed":
mockups = status_data["result"]["mockups"]
if mockups:
return MockupResult(
success=True,
image_url=mockups[0]["mockup_url"],
)
return MockupResult(
success=False,
error="Mockup generation timed out"
)
except Exception as e:
return MockupResult(success=False, error=str(e))
async def get_product(self, product_id: str) -> Optional[dict]:
"""Get sync product details."""
try:
response = await self.client.get(f"/store/products/{product_id}")
response.raise_for_status()
data = response.json()
return data.get("result")
except Exception:
return None
async def list_products(self, limit: int = 100) -> list[dict]:
"""List all sync products."""
try:
response = await self.client.get(
"/store/products",
params={"limit": limit}
)
response.raise_for_status()
data = response.json()
return data.get("result", [])
except Exception:
return []
async def get_catalog(self, product_type: Optional[str] = None) -> list[dict]:
"""Get Printful product catalog."""
try:
response = await self.client.get("/products")
response.raise_for_status()
data = response.json()
products = data.get("result", [])
if product_type:
# Filter by type (approximate matching)
type_map = {
"tshirt": ["t-shirt", "tee"],
"hoodie": ["hoodie", "sweatshirt"],
"sticker": ["sticker"],
}
keywords = type_map.get(product_type, [product_type])
products = [
p for p in products
if any(kw in p.get("title", "").lower() for kw in keywords)
]
return products
except Exception:
return []
async def upload_file(self, file_path: Path) -> Optional[str]:
"""Upload a file to Printful."""
try:
# Printful accepts URLs, so we need to either:
# 1. Use their file library (POST /files)
# 2. Host the file elsewhere and provide URL
# For now, use file library
with open(file_path, "rb") as f:
files = {"file": (file_path.name, f, "image/png")}
response = await self.client.post(
"/files",
files=files,
headers={"Content-Type": None}, # Let httpx set multipart
)
response.raise_for_status()
data = response.json()
return data.get("result", {}).get("preview_url")
except Exception:
return None
def _get_variant_id(self, product_id: int, color: str, size: str) -> int:
"""
Get variant ID for a product+color+size combination.
In production, this would query the catalog to find the right variant.
For now, returns a placeholder.
"""
# TODO: Implement proper variant lookup from catalog
return 0
async def close(self) -> None:
"""Close the HTTP client."""
await self.client.aclose()

275
cli/mycopunk/pod/prodigi.py Normal file
View File

@ -0,0 +1,275 @@
"""
Prodigi API integration.
API Documentation: https://www.prodigi.com/print-api/docs/
"""
from pathlib import Path
from typing import Optional
import httpx
from .base import PODProvider, ProductResult, MockupResult
class ProdigiProvider(PODProvider):
"""Prodigi print-on-demand provider."""
name = "prodigi"
SANDBOX_URL = "https://api.sandbox.prodigi.com/v4.0"
PRODUCTION_URL = "https://api.prodigi.com/v4.0"
def __init__(self, api_key: str, sandbox: bool = False) -> None:
super().__init__(api_key, sandbox)
base_url = self.SANDBOX_URL if sandbox else self.PRODUCTION_URL
self.client = httpx.AsyncClient(
base_url=base_url,
headers={
"X-API-Key": api_key,
"Content-Type": "application/json",
},
timeout=30.0,
)
def _validate_credentials(self) -> None:
"""Validate Prodigi API key."""
if not self.api_key:
raise ValueError("Prodigi API key is required")
async def create_product(
self,
design_path: Path,
metadata: dict,
product_config: dict,
) -> ProductResult:
"""
Create a product template on Prodigi.
Note: Prodigi doesn't have a "product" concept like Printful.
Instead, orders are placed directly with product SKUs and image URLs.
This method creates a "template" in our system for tracking.
"""
try:
# Upload the design file
file_url = await self.upload_file(design_path)
if not file_url:
return ProductResult(
success=False,
error="Failed to upload design file"
)
# Prodigi works on-demand, so we just validate the SKU exists
sku = product_config.get("sku")
# Validate SKU by checking catalog
catalog = await self.get_catalog()
valid_skus = [p.get("sku") for p in catalog]
if sku and sku not in valid_skus:
return ProductResult(
success=False,
error=f"Invalid SKU: {sku}"
)
# Return a "virtual" product ID for tracking
product_id = f"prodigi_{metadata.get('slug')}_{sku}"
return ProductResult(
success=True,
product_id=product_id,
data={
"sku": sku,
"image_url": file_url,
"name": metadata.get("name"),
"retail_price": product_config.get("retail_price"),
},
)
except Exception as e:
return ProductResult(success=False, error=str(e))
async def update_product(
self,
product_id: str,
design_path: Path,
metadata: dict,
) -> ProductResult:
"""Update product by re-uploading the design file."""
try:
file_url = await self.upload_file(design_path)
if not file_url:
return ProductResult(
success=False,
error="Failed to upload design file"
)
return ProductResult(
success=True,
product_id=product_id,
data={"image_url": file_url},
)
except Exception as e:
return ProductResult(success=False, error=str(e))
async def generate_mockup(
self,
product_id: str,
variant: Optional[str] = None,
) -> MockupResult:
"""
Generate product mockup.
Note: Prodigi doesn't have a built-in mockup generator.
This would need to use a third-party service or custom solution.
"""
return MockupResult(
success=False,
error="Prodigi does not provide mockup generation. Use external mockup service."
)
async def get_product(self, product_id: str) -> Optional[dict]:
"""
Get product details.
Since Prodigi doesn't store products, this returns None.
Product data should be stored locally in metadata.yaml.
"""
return None
async def list_products(self, limit: int = 100) -> list[dict]:
"""
List products.
Since Prodigi doesn't store products, returns empty list.
Use local design list instead.
"""
return []
async def get_catalog(self, product_type: Optional[str] = None) -> list[dict]:
"""Get Prodigi product catalog."""
try:
response = await self.client.get("/products")
response.raise_for_status()
data = response.json()
products = data.get("products", [])
if product_type:
# Filter by type
type_keywords = {
"sticker": ["sticker", "sti"],
"print": ["print", "poster", "art"],
"canvas": ["canvas"],
}
keywords = type_keywords.get(product_type, [product_type])
products = [
p for p in products
if any(kw in p.get("sku", "").lower() or
kw in p.get("description", "").lower()
for kw in keywords)
]
return products
except Exception:
return []
async def upload_file(self, file_path: Path) -> Optional[str]:
"""
Upload a file to Prodigi.
Note: Prodigi expects image URLs, not uploaded files.
You need to host the image somewhere accessible.
Options:
1. Use Cloudflare R2/S3
2. Use a public URL from your server
3. Use Prodigi's asset upload (if available)
"""
# For now, return None - in production, implement actual upload
# to your hosting service (R2, S3, etc.)
return None
async def create_order(
self,
sku: str,
image_url: str,
recipient: dict,
quantity: int = 1,
) -> dict:
"""
Create an order on Prodigi.
Args:
sku: Product SKU
image_url: URL to the design image
recipient: Shipping address dict
quantity: Number of items
Returns:
Order data from Prodigi
"""
try:
payload = {
"shippingMethod": "Standard",
"recipient": recipient,
"items": [
{
"sku": sku,
"copies": quantity,
"assets": [
{
"printArea": "default",
"url": image_url,
}
],
}
],
}
response = await self.client.post("/orders", json=payload)
response.raise_for_status()
return response.json()
except Exception as e:
return {"error": str(e)}
async def get_order(self, order_id: str) -> Optional[dict]:
"""Get order status from Prodigi."""
try:
response = await self.client.get(f"/orders/{order_id}")
response.raise_for_status()
return response.json()
except Exception:
return None
async def get_quote(
self,
sku: str,
destination_country: str = "US",
quantity: int = 1,
) -> Optional[dict]:
"""Get a price quote for a product."""
try:
payload = {
"shippingMethod": "Standard",
"destinationCountryCode": destination_country,
"items": [
{
"sku": sku,
"copies": quantity,
}
],
}
response = await self.client.post("/quotes", json=payload)
response.raise_for_status()
return response.json()
except Exception:
return None
async def close(self) -> None:
"""Close the HTTP client."""
await self.client.aclose()

90
cli/pyproject.toml Normal file
View File

@ -0,0 +1,90 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "mycopunk"
version = "0.1.0"
description = "CLI tool for managing mycopunk merchandise and POD fulfillment"
readme = "README.md"
license = "MIT"
requires-python = ">=3.10"
authors = [
{ name = "Jeff Emmett", email = "jeff@jeffemmett.com" }
]
keywords = ["merchandise", "print-on-demand", "cli", "design"]
classifiers = [
"Development Status :: 3 - Alpha",
"Environment :: Console",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Multimedia :: Graphics",
]
dependencies = [
"typer[all]>=0.9.0",
"rich>=13.0.0",
"pyyaml>=6.0",
"httpx>=0.25.0",
"python-dotenv>=1.0.0",
"pillow>=10.0.0",
"pydantic>=2.0.0",
"jinja2>=3.1.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.4.0",
"pytest-cov>=4.1.0",
"pytest-asyncio>=0.21.0",
"black>=23.0.0",
"ruff>=0.1.0",
"mypy>=1.5.0",
"types-PyYAML>=6.0.0",
]
[project.scripts]
mycopunk = "mycopunk.cli:app"
[project.urls]
Homepage = "https://gitea.jeffemmett.com/jeffemmett/mycopunk-swag"
Documentation = "https://gitea.jeffemmett.com/jeffemmett/mycopunk-swag/wiki"
Repository = "https://gitea.jeffemmett.com/jeffemmett/mycopunk-swag.git"
Issues = "https://gitea.jeffemmett.com/jeffemmett/mycopunk-swag/issues"
[tool.hatch.build.targets.wheel]
packages = ["mycopunk"]
[tool.black]
line-length = 100
target-version = ["py310", "py311", "py312"]
[tool.ruff]
line-length = 100
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
]
ignore = [
"E501", # line too long (handled by black)
]
[tool.mypy]
python_version = "3.10"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"

1
cli/tests/__init__.py Normal file
View File

@ -0,0 +1 @@
"""Tests for mycopunk CLI."""

58
cli/tests/test_design.py Normal file
View File

@ -0,0 +1,58 @@
"""Tests for design management commands."""
import pytest
from pathlib import Path
import tempfile
import shutil
from mycopunk.design import create_design, validate_design, list_designs
class TestCreateDesign:
"""Tests for design creation."""
def test_create_design_creates_directory_structure(self, tmp_path):
"""Test that create_design creates the expected directory structure."""
# This would need mocking of DESIGNS_DIR
# For now, this is a placeholder for the test structure
pass
def test_create_design_with_invalid_path_raises_error(self):
"""Test that invalid paths raise an error."""
pass
def test_create_design_with_unknown_template_raises_error(self):
"""Test that unknown templates raise an error."""
pass
class TestValidateDesign:
"""Tests for design validation."""
def test_validate_missing_design_raises_error(self):
"""Test that missing designs raise an error."""
pass
def test_validate_missing_metadata_raises_error(self):
"""Test that missing metadata.yaml raises an error."""
pass
def test_validate_complete_design_passes(self):
"""Test that a complete design passes validation."""
pass
class TestListDesigns:
"""Tests for design listing."""
def test_list_empty_designs_directory(self):
"""Test listing when no designs exist."""
pass
def test_list_with_status_filter(self):
"""Test filtering designs by status."""
pass
def test_list_with_type_filter(self):
"""Test filtering designs by type."""
pass

39
config/.env.example Normal file
View File

@ -0,0 +1,39 @@
# Mycopunk Swag - Environment Variables
# Copy this file to .env and fill in your API credentials
# NEVER commit .env to version control
# ===========================================
# Printful API
# Get your token at: https://developers.printful.com/
# ===========================================
PRINTFUL_API_TOKEN=your_printful_api_token_here
# ===========================================
# Prodigi API
# Get keys at: https://dashboard.prodigi.com/
# ===========================================
# Sandbox key for testing (orders not fulfilled)
PRODIGI_API_KEY_SANDBOX=your_prodigi_sandbox_key_here
# Live key for production orders
PRODIGI_API_KEY_LIVE=your_prodigi_live_key_here
# ===========================================
# Stripe (for custom shop - Phase 5)
# Get keys at: https://dashboard.stripe.com/
# ===========================================
# STRIPE_SECRET_KEY=sk_test_xxx
# STRIPE_WEBHOOK_SECRET=whsec_xxx
# STRIPE_PUBLISHABLE_KEY=pk_test_xxx
# ===========================================
# Application Settings
# ===========================================
# Set to 'production' when ready for live orders
MYCOPUNK_ENV=development
# Default POD provider (printful or prodigi)
DEFAULT_PROVIDER=prodigi
# Enable debug logging
DEBUG=false

232
config/products.yaml Normal file
View File

@ -0,0 +1,232 @@
# Mycopunk Product Catalog Configuration
# Maps design types to POD provider products
defaults:
color_profile: sRGB
resolution: 300
format: png
# Sticker Products
stickers:
small:
name: "3×3 Vinyl Sticker"
dimensions:
width: 3
height: 3
unit: inches
pixels:
width: 900
height: 900
providers:
prodigi:
sku: "GLOBAL-STI-KIS-3X3"
variants:
- id: matte
name: "Matte Finish"
- id: gloss
name: "Gloss Finish"
base_cost: 1.20
printful:
sku: 358 # Kiss-cut stickers
variants:
- id: white
name: "White"
base_cost: 1.50
medium:
name: "4×4 Vinyl Sticker"
dimensions:
width: 4
height: 4
unit: inches
pixels:
width: 1200
height: 1200
providers:
prodigi:
sku: "GLOBAL-STI-KIS-4X4"
variants:
- id: matte
name: "Matte Finish"
- id: gloss
name: "Gloss Finish"
base_cost: 1.80
large:
name: "6×6 Vinyl Sticker"
dimensions:
width: 6
height: 6
unit: inches
pixels:
width: 1800
height: 1800
providers:
prodigi:
sku: "GLOBAL-STI-KIS-6X6"
base_cost: 2.50
# Apparel Products
apparel:
tshirt:
name: "Unisex T-Shirt"
print_areas:
front:
dimensions:
width: 12
height: 16
unit: inches
pixels:
width: 3600
height: 4800
chest:
dimensions:
width: 4
height: 4
unit: inches
pixels:
width: 1200
height: 1200
providers:
printful:
sku: 71 # Bella + Canvas 3001
sizes: [S, M, L, XL, 2XL, 3XL]
colors:
- id: black
name: "Black"
hex: "#0a0a0a"
- id: white
name: "White"
hex: "#ffffff"
- id: heather_charcoal
name: "Heather Charcoal"
hex: "#4a4a4a"
- id: forest_green
name: "Forest Green"
hex: "#2d4a3e"
- id: maroon
name: "Maroon"
hex: "#5a2d2d"
base_cost:
S: 9.25
M: 9.25
L: 9.25
XL: 9.25
2XL: 11.25
3XL: 13.25
hoodie:
name: "Unisex Hoodie"
print_areas:
front:
dimensions:
width: 14
height: 16
unit: inches
pixels:
width: 4200
height: 4800
providers:
printful:
sku: 146 # Bella + Canvas 3719
sizes: [S, M, L, XL, 2XL]
colors:
- id: black
name: "Black"
- id: dark_grey_heather
name: "Dark Grey Heather"
base_cost:
S: 23.95
M: 23.95
L: 23.95
XL: 23.95
2XL: 27.95
# Art Prints
prints:
small:
name: "8×10 Art Print"
dimensions:
width: 8
height: 10
unit: inches
pixels:
width: 2400
height: 3000
providers:
prodigi:
sku: "GLOBAL-FAP-8X10"
variants:
- id: matte
name: "Matte"
- id: lustre
name: "Lustre"
base_cost: 4.50
medium:
name: "11×14 Art Print"
dimensions:
width: 11
height: 14
unit: inches
pixels:
width: 3300
height: 4200
providers:
prodigi:
sku: "GLOBAL-FAP-11X14"
base_cost: 7.00
large:
name: "18×24 Art Print"
dimensions:
width: 18
height: 24
unit: inches
pixels:
width: 5400
height: 7200
providers:
prodigi:
sku: "GLOBAL-FAP-18X24"
base_cost: 12.00
# Pricing Rules
pricing:
default_markup: 2.0 # 100% markup (double cost)
rules:
stickers:
markup: 2.5 # Higher margin on low-cost items
minimum_price: 3.00
apparel:
markup: 1.8
minimum_price: 20.00
prints:
markup: 2.0
minimum_price: 15.00
# Round to nearest .99 or .50
rounding: nearest_99
# Shipping Profiles
shipping:
prodigi:
standard:
name: "Standard"
days: "5-10"
express:
name: "Express"
days: "2-5"
additional_cost: 5.00
printful:
standard:
name: "Standard"
days: "5-12"
express:
name: "Express"
days: "3-5"
additional_cost: 7.00

378
docs/cli-reference.md Normal file
View File

@ -0,0 +1,378 @@
# CLI Reference
Complete command reference for the `mycopunk` CLI tool.
## Installation
```bash
cd cli
pip install -e .
```
This installs the CLI in development mode, allowing you to edit the code and see changes immediately.
## Global Options
```bash
mycopunk --version # Show version
mycopunk --help # Show help
```
---
## Design Commands
### `mycopunk design new`
Create a new design scaffold with metadata template.
```bash
mycopunk design new <path> [--template TEMPLATE]
```
**Arguments:**
- `path` - Design path in format `category/design-name` (e.g., `stickers/my-design`)
**Options:**
- `--template, -t` - Template to use (default: `sticker-3x3`)
**Available Templates:**
| Template | Dimensions | Use Case |
|----------|-----------|----------|
| `sticker-3x3` | 900×900 px | Small vinyl sticker |
| `sticker-4x4` | 1200×1200 px | Medium vinyl sticker |
| `sticker-6x6` | 1800×1800 px | Large vinyl sticker |
| `tshirt-front` | 3600×4800 px | Full front print |
| `tshirt-chest` | 1200×1200 px | Chest/pocket print |
| `print-8x10` | 2400×3000 px | 8×10 art print |
**Example:**
```bash
mycopunk design new stickers/spore-spiral --template=sticker-3x3
mycopunk design new shirts/mycelium-network --template=tshirt-front
```
**Creates:**
```
designs/stickers/spore-spiral/
├── spore-spiral.svg # Edit this in Inkscape
├── metadata.yaml # Configure products here
└── exports/
├── 300dpi/.gitkeep
└── mockups/.gitkeep
```
---
### `mycopunk design list`
List all designs in the repository.
```bash
mycopunk design list [--status STATUS] [--type TYPE]
```
**Options:**
- `--status, -s` - Filter by status (draft, active, retired)
- `--type, -t` - Filter by category (stickers, shirts, misc)
**Example:**
```bash
mycopunk design list
mycopunk design list --status=active
mycopunk design list --type=stickers --status=active
```
---
### `mycopunk design export`
Export design to print-ready files.
```bash
mycopunk design export <path> [--dpi DPI] [--format FORMAT]
```
**Arguments:**
- `path` - Design path (e.g., `stickers/my-design`)
**Options:**
- `--dpi` - Export resolution (default: 300)
- `--format, -f` - Export format: png, jpg (default: png)
**Example:**
```bash
mycopunk design export stickers/spore-spiral
mycopunk design export stickers/spore-spiral --dpi=600
```
**Requirements:**
- Inkscape must be installed for SVG export
- PIL/Pillow for raster format conversion
---
### `mycopunk design validate`
Validate design meets POD requirements.
```bash
mycopunk design validate <path> [--strict]
```
**Arguments:**
- `path` - Design path to validate
**Options:**
- `--strict` - Fail on warnings (not just errors)
**Checks:**
- ✓ Required metadata fields present
- ✓ Source file exists
- ✓ Exported PNG dimensions match spec
- ✓ Resolution meets minimum (300 DPI)
- ✓ Products configured correctly
**Example:**
```bash
mycopunk design validate stickers/spore-spiral
mycopunk design validate stickers/spore-spiral --strict
```
---
## Product Commands
### `mycopunk product create`
Create product on POD service from design.
```bash
mycopunk product create <path> [--provider PROVIDER] [--sandbox]
```
**Arguments:**
- `path` - Design path
**Options:**
- `--provider, -p` - POD provider: printful, prodigi (default: prodigi)
- `--sandbox` - Use sandbox/test mode (no real orders)
**Example:**
```bash
mycopunk product create stickers/spore-spiral --provider=prodigi --sandbox
mycopunk product create shirts/mycelium-network --provider=printful
```
---
### `mycopunk product push`
Push design updates to POD product.
```bash
mycopunk product push <path> [--provider PROVIDER]
```
**Arguments:**
- `path` - Design path
**Options:**
- `--provider, -p` - Specific provider (default: all configured)
**Example:**
```bash
mycopunk product push stickers/spore-spiral
mycopunk product push stickers/spore-spiral --provider=printful
```
---
### `mycopunk product list`
List all products by provider.
```bash
mycopunk product list [--provider PROVIDER]
```
**Options:**
- `--provider, -p` - Filter by provider
**Example:**
```bash
mycopunk product list
mycopunk product list --provider=printful
```
---
## Mockup Commands
### `mycopunk mockup generate`
Generate product mockups for a design.
```bash
mycopunk mockup generate <path> [--all] [--color COLOR] [--size SIZE]
```
**Arguments:**
- `path` - Design path
**Options:**
- `--all` - Generate all variant mockups
- `--color, -c` - Product color (e.g., black, white)
- `--size, -s` - Product size (e.g., M, L, XL)
**Example:**
```bash
mycopunk mockup generate stickers/spore-spiral --all
mycopunk mockup generate shirts/mycelium-network --color=black --size=L
```
---
## Catalog Commands
### `mycopunk catalog sync`
Sync product catalog from POD services.
```bash
mycopunk catalog sync [--provider PROVIDER]
```
**Options:**
- `--provider, -p` - Provider to sync: printful, prodigi, all (default: all)
**Example:**
```bash
mycopunk catalog sync
mycopunk catalog sync --provider=printful
```
---
## Batch Commands
### `mycopunk batch export`
Export all designs matching criteria.
```bash
mycopunk batch export [--type TYPE] [--status STATUS]
```
**Options:**
- `--type, -t` - Filter by category
- `--status, -s` - Filter by status (default: active)
**Example:**
```bash
mycopunk batch export --type=sticker
mycopunk batch export --status=active
```
---
### `mycopunk batch push`
Push all designs to POD services.
```bash
mycopunk batch push [--status STATUS] [--provider PROVIDER]
```
**Options:**
- `--status, -s` - Filter by status (default: active)
- `--provider, -p` - Specific provider
**Example:**
```bash
mycopunk batch push --status=active
mycopunk batch push --provider=prodigi
```
---
## Reports
### `mycopunk report`
Generate various reports.
```bash
mycopunk report <type> [--output FILE]
```
**Arguments:**
- `type` - Report type: pricing, inventory, orders
**Options:**
- `--output, -o` - Output file path
**Example:**
```bash
mycopunk report pricing
mycopunk report pricing --output=pricing.csv
```
---
## Environment Variables
Set these in `config/.env`:
| Variable | Description | Required |
|----------|-------------|----------|
| `PRINTFUL_API_TOKEN` | Printful API token | For Printful operations |
| `PRODIGI_API_KEY_SANDBOX` | Prodigi sandbox key | For testing |
| `PRODIGI_API_KEY_LIVE` | Prodigi production key | For live orders |
| `MYCOPUNK_ENV` | Environment: development/production | No (default: development) |
| `DEFAULT_PROVIDER` | Default POD provider | No (default: prodigi) |
| `DEBUG` | Enable debug logging | No (default: false) |
---
## Exit Codes
| Code | Meaning |
|------|---------|
| 0 | Success |
| 1 | Error (validation failed, API error, etc.) |
---
## Tips
### Quick Workflow
```bash
# Create a new sticker design
mycopunk design new stickers/mushroom-logo --template=sticker-3x3
# Edit the SVG in Inkscape
inkscape designs/stickers/mushroom-logo/mushroom-logo.svg
# Edit metadata.yaml to set pricing
# Export to PNG
mycopunk design export stickers/mushroom-logo
# Validate before pushing
mycopunk design validate stickers/mushroom-logo
# Create on Prodigi (test mode)
mycopunk product create stickers/mushroom-logo --provider=prodigi --sandbox
```
### Batch Operations
```bash
# Export all active stickers
mycopunk batch export --type=stickers --status=active
# Push everything to production
MYCOPUNK_ENV=production mycopunk batch push --status=active
```

261
docs/design-guidelines.md Normal file
View File

@ -0,0 +1,261 @@
# Design Guidelines
This document covers technical requirements and best practices for creating print-ready designs for mycopunk merchandise.
## General Requirements
### File Formats
| Format | Use Case | Notes |
|--------|----------|-------|
| **SVG** | Source files | Preferred for all designs; scales infinitely |
| **PNG** | Print exports | Transparent background, 300 DPI |
| **JPEG** | Photo-based art | Only when transparency not needed |
### Resolution
**All exports must be 300 DPI minimum.**
| Product | Physical Size | Pixels @ 300 DPI |
|---------|---------------|------------------|
| T-shirt (front) | 12" × 16" | 3600 × 4800 px |
| T-shirt (chest pocket) | 4" × 4" | 1200 × 1200 px |
| Hoodie (front) | 14" × 16" | 4200 × 4800 px |
| Sticker (small) | 3" × 3" | 900 × 900 px |
| Sticker (medium) | 4" × 4" | 1200 × 1200 px |
| Sticker (large) | 6" × 6" | 1800 × 1800 px |
| Art print (8×10) | 8" × 10" | 2400 × 3000 px |
| Art print (11×14) | 11" × 14" | 3300 × 4200 px |
### Color Profile
- **sRGB** for all designs
- Avoid CMYK (POD services convert automatically)
- Be aware colors may shift slightly in print
### Transparency
- Use transparent backgrounds for most designs
- POD services can apply designs to any color product
- Exception: full-coverage prints (all-over print shirts)
---
## Sticker-Specific Guidelines
### Types
| Type | Description | Best For |
|------|-------------|----------|
| **Die-cut** | Cut to shape of design | Logos, characters |
| **Kiss-cut** | Cut on backing, easy peel | Complex shapes |
| **Rectangle** | Standard rectangle cut | Text-heavy designs |
### Design Tips
1. **Bleed area**: Add 1/16" (0.0625") bleed around edges
2. **Safe zone**: Keep important elements 1/8" from edge
3. **Minimum line width**: 0.02" for visibility
4. **White border**: Add 1/16" white border if design touches edges
### Prodigi Sticker Specs
```yaml
# Standard kiss-cut vinyl sticker
format: PNG
resolution: 300 DPI
color_mode: sRGB
background: transparent
max_file_size: 25 MB
```
---
## Apparel-Specific Guidelines
### Print Areas
```
┌─────────────────────────────────────┐
│ T-SHIRT FRONT │
│ │
│ ┌───────────────────────┐ │
│ │ │ │
│ │ MAX PRINT AREA │ │
│ │ 12" × 16" │ │
│ │ 3600 × 4800 px │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ └───────────────────────┘ │
│ │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ CHEST/POCKET AREA │
│ │
│ ┌─────────┐ │
│ │ 4" × 4" │ Left chest │
│ │ │ 1200 × 1200 px │
│ └─────────┘ │
│ │
└─────────────────────────────────────┘
```
### Design Tips
1. **Avoid edges**: Design won't print to garment edges
2. **Consider fabric color**: Light designs on dark shirts, vice versa
3. **Simplify gradients**: DTG printing can struggle with subtle gradients
4. **Minimum details**: Small text/lines may not print clearly
5. **Test at actual size**: Print a test at 100% scale before ordering
### Printful Apparel Specs
```yaml
# Front print area
format: PNG
resolution: 300 DPI
dimensions: 3600 × 4800 px (12" × 16")
color_mode: sRGB
background: transparent
max_file_size: 200 MB
```
---
## Art Print Guidelines
### Paper Types (Prodigi)
| Type | Finish | Best For |
|------|--------|----------|
| Matte | No glare | Photos, illustrations |
| Glossy | Reflective | Vibrant colors, pop art |
| Fine Art | Textured | Museum-quality pieces |
### Sizing
Standard sizes match common frames:
- 5" × 7"
- 8" × 10"
- 11" × 14"
- 16" × 20"
- 18" × 24"
### Bleed & Safe Zone
```
┌─────────────────────────────────────┐
│ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ │ ← Bleed (0.125")
│ ▒ ┌───────────────────────────┐ ▒ │
│ ▒ │ │ ▒ │
│ ▒ │ SAFE ZONE │ ▒ │ ← Safe zone (0.25" from trim)
│ ▒ │ Keep text/important │ ▒ │
│ ▒ │ elements here │ ▒ │
│ ▒ │ │ ▒ │
│ ▒ └───────────────────────────┘ ▒ │
│ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ │
└─────────────────────────────────────┘
↑ Trim line
```
---
## Inkscape Workflow
### New Design Setup
1. Create new document: `File > New`
2. Set document properties (`Ctrl+Shift+D`):
- Units: pixels
- Width/Height: Per product specs
- Scale: 1 px = 1 px
3. Save as `.svg` in design directory
### Export Settings
```bash
# CLI export command
inkscape design.svg \
--export-type=png \
--export-dpi=300 \
--export-background-opacity=0 \
--export-filename=design-300dpi.png
```
### Batch Export Script
```bash
#!/bin/bash
# scripts/batch-export.sh
for svg in designs/**/*.svg; do
dir=$(dirname "$svg")
base=$(basename "$svg" .svg)
mkdir -p "$dir/exports/300dpi"
inkscape "$svg" \
--export-type=png \
--export-dpi=300 \
--export-background-opacity=0 \
--export-filename="$dir/exports/300dpi/${base}.png"
done
```
---
## GIMP Workflow
### For Raster Designs
1. Create new image at target pixel dimensions
2. Set resolution: `Image > Print Size > 300 pixels/inch`
3. Work in RGB mode
4. Export as PNG with transparency
### Converting GIMP to Print-Ready
```bash
# Export from GIMP command line
gimp -i -b '(python-fu-export-png RUN-NONINTERACTIVE "input.xcf" "output.png")' -b '(gimp-quit 0)'
```
---
## Quality Checklist
Before exporting, verify:
- [ ] Resolution is 300 DPI or higher
- [ ] Dimensions match product requirements
- [ ] Color profile is sRGB
- [ ] Background is transparent (unless intentional)
- [ ] No text too small (minimum 8pt at print size)
- [ ] No lines thinner than 0.5pt
- [ ] Bleed area included where required
- [ ] Important elements within safe zone
## Validation
Run the CLI validator before pushing:
```bash
mycopunk design validate stickers/my-design
# Output example:
# ✓ Resolution: 300 DPI
# ✓ Dimensions: 900 × 900 px (3" × 3")
# ✓ Color profile: sRGB
# ✓ Transparency: Yes
# ✗ Warning: Design extends into bleed area
```
---
## Resources
- [Printful File Guidelines](https://www.printful.com/blog/everything-you-need-to-know-to-prepare-the-perfect-printfile)
- [Prodigi Print Specs](https://www.prodigi.com/print-api/docs/reference/#images)
- [Inkscape Documentation](https://inkscape.org/doc/)
- [GIMP Documentation](https://www.gimp.org/docs/)

321
docs/workflow.md Normal file
View File

@ -0,0 +1,321 @@
# Mycopunk Workflow Guide
This document describes the end-to-end workflow for creating, managing, and fulfilling mycopunk merchandise.
## Overview
```
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Design │ → │ Export │ → │ Publish │ → │ Fulfill │
│ (Inkscape) │ │ (CLI) │ │ (POD) │ │ (Orders) │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
```
## Phase 1: Design Creation
### 1.1 Create Design Scaffold
```bash
# Create a new sticker design
mycopunk design new stickers/mycelium-mandala --template=sticker-4x4
```
This creates:
```
designs/stickers/mycelium-mandala/
├── mycelium-mandala.svg # Source file
├── metadata.yaml # Configuration
└── exports/ # Output directory
```
### 1.2 Design in Inkscape
Open the SVG file in Inkscape:
```bash
inkscape designs/stickers/mycelium-mandala/mycelium-mandala.svg
```
**Design Guidelines:**
- Work at the specified canvas size (e.g., 1200×1200 px for 4×4" sticker)
- Use sRGB color profile
- Keep important elements away from edges (safe zone)
- Add 1/16" bleed if design touches edges
- Save frequently!
### 1.3 Configure Metadata
Edit `metadata.yaml`:
```yaml
name: "Mycelium Mandala"
slug: mycelium-mandala
description: "Sacred geometry inspired by fungal networks"
tags: [mycelium, mandala, sacred-geometry, nature]
created: 2025-01-24
author: jeff
source:
file: mycelium-mandala.svg
format: svg
dimensions:
width: 1200
height: 1200
dpi: 300
color_profile: sRGB
products:
- type: sticker
provider: prodigi
sku: GLOBAL-STI-KIS-4X4
variants: [matte, gloss]
retail_price: 4.50
- type: sticker
provider: printful
sku: 358
variants: [white]
retail_price: 4.50
status: draft # Change to 'active' when ready
```
---
## Phase 2: Export & Validate
### 2.1 Export to Print-Ready Files
```bash
mycopunk design export stickers/mycelium-mandala
```
Output:
```
✓ Exported: designs/stickers/mycelium-mandala/exports/300dpi/mycelium-mandala.png
Size: 245.3 KB
Dimensions: 1200×1200 px
```
### 2.2 Validate Design
```bash
mycopunk design validate stickers/mycelium-mandala
```
Output:
```
Validating: Mycelium Mandala
✓ Metadata field 'name' present
✓ Metadata field 'slug' present
✓ Metadata field 'source' present
✓ Metadata field 'products' present
✓ Metadata field 'status' present
✓ Source file exists: mycelium-mandala.svg
✓ Dimensions correct: 1200×1200
✓ Products configured: 2
✓ Validation passed
```
### 2.3 Generate Local Mockups (Optional)
For quick previews without API calls:
```bash
./scripts/generate-mockups.sh stickers/mycelium-mandala
```
---
## Phase 3: Publish to POD
### 3.1 Test in Sandbox First
Always test with sandbox credentials:
```bash
mycopunk product create stickers/mycelium-mandala --provider=prodigi --sandbox
```
### 3.2 Update Status
Once satisfied, update the design status:
```yaml
# In metadata.yaml
status: active
```
### 3.3 Create Production Product
```bash
# Ensure MYCOPUNK_ENV=production or use live API keys
mycopunk product create stickers/mycelium-mandala --provider=prodigi
```
### 3.4 Generate Production Mockups
```bash
mycopunk mockup generate stickers/mycelium-mandala --all
```
---
## Phase 4: Selling & Fulfillment
### Option A: Use POD Storefronts
Both Printful and Prodigi integrate with:
- Etsy
- Shopify
- WooCommerce
- Amazon
Connect your accounts in their dashboards.
### Option B: Custom Shop (Phase 5)
Build your own storefront (see architecture in plan).
### Order Flow
```
Customer Order → Payment → API Call to POD → Fulfillment → Shipping
```
---
## Batch Operations
### Export All Active Designs
```bash
mycopunk batch export --status=active
```
### Push All to POD
```bash
mycopunk batch push --status=active --provider=prodigi
```
### Generate Price Report
```bash
mycopunk report pricing > pricing.csv
```
---
## File Organization Best Practices
### Design Directory Structure
```
designs/
├── stickers/
│ ├── mycelium-mandala/
│ │ ├── mycelium-mandala.svg # Source (edit this)
│ │ ├── mycelium-mandala.xcf # GIMP working file (optional)
│ │ ├── metadata.yaml # Configuration
│ │ ├── reference/ # Inspiration images (optional)
│ │ └── exports/
│ │ ├── 300dpi/
│ │ │ └── mycelium-mandala.png
│ │ └── mockups/
│ │ ├── sticker-mockup.png
│ │ └── product-photo.jpg
│ └── spore-spiral/
│ └── ...
├── shirts/
│ └── network-tee/
│ └── ...
└── misc/
├── patches/
├── pins/
└── zines/
```
### Naming Conventions
- **Slugs**: lowercase, hyphen-separated (e.g., `mycelium-mandala`)
- **Files**: Match slug (e.g., `mycelium-mandala.svg`)
- **Categories**: Plural (e.g., `stickers/`, `shirts/`)
---
## Git Workflow
### Committing Designs
```bash
# Add new design
git add designs/stickers/mycelium-mandala/
# Don't commit exports (they're gitignored)
# They can be regenerated with mycopunk design export
git commit -m "feat(stickers): add mycelium mandala design"
```
### Branching Strategy
```
main # Production-ready designs
└── dev # Work in progress
└── feature/new-sticker-pack
```
---
## Troubleshooting
### Export Fails
**Problem**: Inkscape export produces error
**Solution**:
1. Check Inkscape is installed: `which inkscape`
2. Verify SVG is valid: open in Inkscape GUI
3. Check file permissions
### Validation Warnings
**Problem**: "Low resolution" warning
**Solution**: Ensure source SVG canvas matches target dimensions at 300 DPI
**Problem**: "No exported PNG found"
**Solution**: Run `mycopunk design export <path>` first
### API Errors
**Problem**: "401 Unauthorized"
**Solution**: Check API keys in `config/.env`
**Problem**: "Invalid SKU"
**Solution**: Sync catalog and verify SKU exists:
```bash
./scripts/sync-catalog.sh prodigi
jq '.products[] | .sku' .cache/catalogs/prodigi-catalog.json | grep -i sticker
```
---
## Quick Reference
| Task | Command |
|------|---------|
| New design | `mycopunk design new <path> --template=<t>` |
| List designs | `mycopunk design list` |
| Export design | `mycopunk design export <path>` |
| Validate design | `mycopunk design validate <path>` |
| Create product | `mycopunk product create <path> --provider=<p>` |
| Push updates | `mycopunk product push <path>` |
| Generate mockups | `mycopunk mockup generate <path>` |
| Batch export | `mycopunk batch export --type=<t>` |

75
scripts/batch-export.sh Executable file
View File

@ -0,0 +1,75 @@
#!/bin/bash
# Batch export all SVG designs to PNG at 300 DPI
#
# Usage: ./scripts/batch-export.sh [category]
# Example: ./scripts/batch-export.sh stickers
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
DESIGNS_DIR="$PROJECT_DIR/designs"
DPI=${DPI:-300}
CATEGORY=${1:-}
echo "🍄 Mycopunk Batch Export"
echo "========================"
echo "DPI: $DPI"
echo ""
# Check for Inkscape
if ! command -v inkscape &> /dev/null; then
echo "Error: Inkscape not found. Please install Inkscape."
echo "Ubuntu: sudo apt install inkscape"
exit 1
fi
count=0
errors=0
# Find all SVG files
find_pattern="$DESIGNS_DIR"
if [ -n "$CATEGORY" ]; then
find_pattern="$DESIGNS_DIR/$CATEGORY"
echo "Category: $CATEGORY"
fi
echo ""
for svg in $(find "$find_pattern" -name "*.svg" -type f 2>/dev/null); do
dir=$(dirname "$svg")
base=$(basename "$svg" .svg)
export_dir="$dir/exports/${DPI}dpi"
output="$export_dir/${base}.png"
# Skip template files
if [[ "$dir" == *"templates"* ]]; then
continue
fi
echo "Exporting: $base"
mkdir -p "$export_dir"
if inkscape "$svg" \
--export-type=png \
--export-dpi="$DPI" \
--export-background-opacity=0 \
--export-filename="$output" 2>/dev/null; then
echo "$output"
((count++))
else
echo " ✗ Failed"
((errors++))
fi
done
echo ""
echo "========================"
echo "Exported: $count files"
if [ $errors -gt 0 ]; then
echo "Errors: $errors"
exit 1
fi
echo "Done!"

74
scripts/generate-mockups.sh Executable file
View File

@ -0,0 +1,74 @@
#!/bin/bash
# Generate mockups for designs using ImageMagick compositing
#
# This is a basic local mockup generator for quick previews.
# For production mockups, use the POD APIs (Printful has great mockup generation).
#
# Usage: ./scripts/generate-mockups.sh <design-path> [template]
# Example: ./scripts/generate-mockups.sh stickers/spore-spiral sticker-3x3
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
DESIGNS_DIR="$PROJECT_DIR/designs"
TEMPLATES_DIR="$PROJECT_DIR/templates/mockups"
DESIGN_PATH=${1:-}
TEMPLATE=${2:-}
if [ -z "$DESIGN_PATH" ]; then
echo "Usage: $0 <design-path> [template]"
echo "Example: $0 stickers/spore-spiral sticker-3x3"
exit 1
fi
# Check for ImageMagick
if ! command -v convert &> /dev/null; then
echo "Error: ImageMagick not found. Please install ImageMagick."
echo "Ubuntu: sudo apt install imagemagick"
exit 1
fi
DESIGN_DIR="$DESIGNS_DIR/$DESIGN_PATH"
EXPORT_DIR="$DESIGN_DIR/exports/300dpi"
MOCKUP_DIR="$DESIGN_DIR/exports/mockups"
# Find the exported PNG
DESIGN_PNG=$(find "$EXPORT_DIR" -name "*.png" -type f | head -1)
if [ -z "$DESIGN_PNG" ]; then
echo "Error: No exported PNG found in $EXPORT_DIR"
echo "Run 'mycopunk design export $DESIGN_PATH' first"
exit 1
fi
echo "🍄 Generating mockups for: $DESIGN_PATH"
echo "Using: $(basename "$DESIGN_PNG")"
echo ""
mkdir -p "$MOCKUP_DIR"
# Simple sticker mockup - add drop shadow and slight rotation
echo "Creating sticker mockup..."
convert "$DESIGN_PNG" \
\( +clone -background black -shadow 60x5+5+5 \) \
+swap -background white -layers merge +repage \
-rotate -5 \
"$MOCKUP_DIR/sticker-mockup.png"
echo " ✓ sticker-mockup.png"
# T-shirt mockup placeholder
# In production, use Printful's mockup API for photorealistic results
echo "Creating simple t-shirt preview..."
convert -size 500x600 xc:'#2a2a2a' \
"$DESIGN_PNG" -resize 200x200 -gravity center -composite \
-gravity north -fill white -pointsize 14 -annotate +0+10 "T-SHIRT PREVIEW" \
"$MOCKUP_DIR/tshirt-preview.png"
echo " ✓ tshirt-preview.png"
echo ""
echo "Mockups saved to: $MOCKUP_DIR"
echo ""
echo "Note: For production-quality mockups, use the POD provider APIs:"
echo " mycopunk mockup generate $DESIGN_PATH --provider=printful"

85
scripts/sync-catalog.sh Executable file
View File

@ -0,0 +1,85 @@
#!/bin/bash
# Sync product catalogs from POD providers
#
# Fetches the latest product catalog from Printful and Prodigi
# and caches locally for offline reference.
#
# Usage: ./scripts/sync-catalog.sh [provider]
# Example: ./scripts/sync-catalog.sh printful
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
CACHE_DIR="$PROJECT_DIR/.cache/catalogs"
PROVIDER=${1:-all}
mkdir -p "$CACHE_DIR"
echo "🍄 Syncing POD Catalogs"
echo "======================="
echo ""
sync_printful() {
echo "Syncing Printful catalog..."
if [ -z "$PRINTFUL_API_TOKEN" ]; then
echo " ⚠ PRINTFUL_API_TOKEN not set, skipping"
return
fi
curl -s -H "Authorization: Bearer $PRINTFUL_API_TOKEN" \
"https://api.printful.com/products" \
> "$CACHE_DIR/printful-catalog.json"
count=$(jq '.result | length' "$CACHE_DIR/printful-catalog.json" 2>/dev/null || echo "0")
echo " ✓ Saved $count products to printful-catalog.json"
}
sync_prodigi() {
echo "Syncing Prodigi catalog..."
API_KEY="${PRODIGI_API_KEY_SANDBOX:-$PRODIGI_API_KEY_LIVE}"
if [ -z "$API_KEY" ]; then
echo " ⚠ PRODIGI_API_KEY not set, skipping"
return
fi
curl -s -H "X-API-Key: $API_KEY" \
"https://api.sandbox.prodigi.com/v4.0/products" \
> "$CACHE_DIR/prodigi-catalog.json"
count=$(jq '.products | length' "$CACHE_DIR/prodigi-catalog.json" 2>/dev/null || echo "0")
echo " ✓ Saved $count products to prodigi-catalog.json"
}
# Load environment
if [ -f "$PROJECT_DIR/config/.env" ]; then
export $(cat "$PROJECT_DIR/config/.env" | grep -v '^#' | xargs)
fi
case $PROVIDER in
printful)
sync_printful
;;
prodigi)
sync_prodigi
;;
all)
sync_printful
sync_prodigi
;;
*)
echo "Unknown provider: $PROVIDER"
echo "Usage: $0 [printful|prodigi|all]"
exit 1
;;
esac
echo ""
echo "Catalogs cached in: $CACHE_DIR"
echo ""
echo "View catalogs with:"
echo " jq '.result[] | {id, title}' $CACHE_DIR/printful-catalog.json"
echo " jq '.products[] | {sku, description}' $CACHE_DIR/prodigi-catalog.json"

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Printful Sticker Template
Dimensions: 3" × 3" at 300 DPI = 900 × 900 px
Kiss-cut sticker with bleed area.
-->
<svg xmlns="http://www.w3.org/2000/svg"
width="900"
height="900"
viewBox="0 0 900 900">
<!-- Background (transparent in final export) -->
<rect width="100%" height="100%" fill="none"/>
<!-- Bleed Area (extend design to here) -->
<rect x="0" y="0" width="900" height="900"
fill="none" stroke="#ff0000" stroke-width="2" stroke-dasharray="10,5"
opacity="0.3"/>
<!-- Cut Line (actual sticker edge) -->
<rect x="19" y="19" width="862" height="862"
fill="none" stroke="#ff00ff" stroke-width="2"
opacity="0.5"/>
<!-- Safe Zone (keep important elements inside) -->
<rect x="38" y="38" width="824" height="824"
fill="none" stroke="#00ff00" stroke-width="2" stroke-dasharray="15,10"
opacity="0.5"/>
<!-- Center Guides -->
<line x1="450" y1="0" x2="450" y2="900"
stroke="#0088ff" stroke-width="1" stroke-dasharray="5,5" opacity="0.3"/>
<line x1="0" y1="450" x2="900" y2="450"
stroke="#0088ff" stroke-width="1" stroke-dasharray="5,5" opacity="0.3"/>
<!-- Template Info (delete before export) -->
<text x="450" y="50" text-anchor="middle" fill="#999" font-family="sans-serif" font-size="24">
3" × 3" STICKER (900×900 px)
</text>
<!-- Guide Legend -->
<g transform="translate(50, 800)" font-family="sans-serif" font-size="14">
<rect x="0" y="0" width="20" height="10" fill="none" stroke="#ff0000" stroke-width="2"/>
<text x="25" y="10" fill="#999">Bleed</text>
<rect x="100" y="0" width="20" height="10" fill="none" stroke="#ff00ff" stroke-width="2"/>
<text x="125" y="10" fill="#999">Cut</text>
<rect x="180" y="0" width="20" height="10" fill="none" stroke="#00ff00" stroke-width="2"/>
<text x="205" y="10" fill="#999">Safe</text>
</g>
<!-- Your design goes here -->
<g id="design">
<text x="450" y="450" text-anchor="middle" fill="#ccc" font-family="sans-serif" font-size="36">
DESIGN
</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Printful T-Shirt Front Print Template
Dimensions: 12" × 16" at 300 DPI = 3600 × 4800 px
Use this as a reference for print area positioning.
-->
<svg xmlns="http://www.w3.org/2000/svg"
width="3600"
height="4800"
viewBox="0 0 3600 4800">
<!-- Background (transparent in final export) -->
<rect width="100%" height="100%" fill="none"/>
<!-- Safe Zone Guide (keep important elements inside) -->
<rect x="100" y="100" width="3400" height="4600"
fill="none" stroke="#00ff00" stroke-width="2" stroke-dasharray="20,10"
opacity="0.5"/>
<!-- Center Guides -->
<line x1="1800" y1="0" x2="1800" y2="4800"
stroke="#0088ff" stroke-width="1" stroke-dasharray="10,10" opacity="0.3"/>
<line x1="0" y1="2400" x2="3600" y2="2400"
stroke="#0088ff" stroke-width="1" stroke-dasharray="10,10" opacity="0.3"/>
<!-- Template Info (delete before export) -->
<text x="1800" y="100" text-anchor="middle" fill="#999" font-family="sans-serif" font-size="48">
PRINTFUL T-SHIRT FRONT - 12" × 16" (3600×4800 px)
</text>
<text x="1800" y="4750" text-anchor="middle" fill="#999" font-family="sans-serif" font-size="36">
Delete this text layer before exporting
</text>
<!-- Your design goes here -->
<!-- Replace everything below with your actual design -->
<g id="design">
<text x="1800" y="2400" text-anchor="middle" fill="#ccc" font-family="sans-serif" font-size="72">
YOUR DESIGN HERE
</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Prodigi Art Print Template
Dimensions: 8" × 10" at 300 DPI = 2400 × 3000 px
Available finishes:
- Matte
- Lustre
- Fine Art (textured)
-->
<svg xmlns="http://www.w3.org/2000/svg"
width="2400"
height="3000"
viewBox="0 0 2400 3000">
<!-- Background (white for prints, or your design's background) -->
<rect width="100%" height="100%" fill="#ffffff"/>
<!-- Bleed Area (1/8" = 38px at 300 DPI) -->
<rect x="0" y="0" width="2400" height="3000"
fill="none" stroke="#ff0000" stroke-width="2" stroke-dasharray="20,10"
opacity="0.3"/>
<!-- Trim Line (final print edge) -->
<rect x="38" y="38" width="2324" height="2924"
fill="none" stroke="#ff00ff" stroke-width="2"
opacity="0.5"/>
<!-- Safe Zone (1/4" from trim = 75px) -->
<rect x="113" y="113" width="2174" height="2774"
fill="none" stroke="#00ff00" stroke-width="2" stroke-dasharray="25,12"
opacity="0.5"/>
<!-- Center Guides -->
<line x1="1200" y1="0" x2="1200" y2="3000"
stroke="#0088ff" stroke-width="1" stroke-dasharray="10,10" opacity="0.2"/>
<line x1="0" y1="1500" x2="2400" y2="1500"
stroke="#0088ff" stroke-width="1" stroke-dasharray="10,10" opacity="0.2"/>
<!-- Rule of Thirds Grid -->
<line x1="800" y1="0" x2="800" y2="3000"
stroke="#ffaa00" stroke-width="1" stroke-dasharray="5,15" opacity="0.2"/>
<line x1="1600" y1="0" x2="1600" y2="3000"
stroke="#ffaa00" stroke-width="1" stroke-dasharray="5,15" opacity="0.2"/>
<line x1="0" y1="1000" x2="2400" y2="1000"
stroke="#ffaa00" stroke-width="1" stroke-dasharray="5,15" opacity="0.2"/>
<line x1="0" y1="2000" x2="2400" y2="2000"
stroke="#ffaa00" stroke-width="1" stroke-dasharray="5,15" opacity="0.2"/>
<!-- Template Header -->
<text x="1200" y="60" text-anchor="middle" fill="#999" font-family="sans-serif" font-size="36">
PRODIGI ART PRINT - 8" × 10" (2400×3000 px)
</text>
<!-- Guide Legend -->
<g transform="translate(100, 2920)" font-family="sans-serif" font-size="18">
<line x1="0" y1="0" x2="40" y2="0" stroke="#ff0000" stroke-width="2" stroke-dasharray="8,4"/>
<text x="50" y="6" fill="#999">Bleed</text>
<line x1="200" y1="0" x2="240" y2="0" stroke="#ff00ff" stroke-width="2"/>
<text x="250" y="6" fill="#999">Trim</text>
<line x1="400" y1="0" x2="440" y2="0" stroke="#00ff00" stroke-width="2" stroke-dasharray="10,5"/>
<text x="450" y="6" fill="#999">Safe zone</text>
<line x1="650" y1="0" x2="690" y2="0" stroke="#ffaa00" stroke-width="1" stroke-dasharray="5,5"/>
<text x="700" y="6" fill="#999">Rule of thirds</text>
</g>
<!-- Design Area -->
<g id="design">
<text x="1200" y="1480" text-anchor="middle" fill="#ddd" font-family="sans-serif" font-size="64">
YOUR ARTWORK
</text>
<text x="1200" y="1560" text-anchor="middle" fill="#ccc" font-family="sans-serif" font-size="32">
Replace this layer with your design
</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Prodigi Vinyl Kiss-Cut Sticker Template
Multiple sizes supported - adjust dimensions as needed.
Common sizes:
- 3" × 3" = 900 × 900 px
- 4" × 4" = 1200 × 1200 px
- 6" × 6" = 1800 × 1800 px
This template is for 4" × 4" (1200 × 1200 px)
-->
<svg xmlns="http://www.w3.org/2000/svg"
width="1200"
height="1200"
viewBox="0 0 1200 1200">
<!-- Background (transparent in final export) -->
<rect width="100%" height="100%" fill="none"/>
<!-- Bleed Area (1/16" = ~19px at 300 DPI) -->
<rect x="0" y="0" width="1200" height="1200"
fill="none" stroke="#ff0000" stroke-width="2" stroke-dasharray="15,8"
opacity="0.3"/>
<!-- Cut Line -->
<rect x="19" y="19" width="1162" height="1162"
fill="none" stroke="#ff00ff" stroke-width="2"
opacity="0.5"/>
<!-- Safe Zone (1/8" from cut = ~38px) -->
<rect x="57" y="57" width="1086" height="1086"
fill="none" stroke="#00ff00" stroke-width="2" stroke-dasharray="20,10"
opacity="0.5"/>
<!-- Center Cross -->
<line x1="600" y1="0" x2="600" y2="1200"
stroke="#0088ff" stroke-width="1" stroke-dasharray="8,8" opacity="0.3"/>
<line x1="0" y1="600" x2="1200" y2="600"
stroke="#0088ff" stroke-width="1" stroke-dasharray="8,8" opacity="0.3"/>
<!-- Template Header -->
<text x="600" y="40" text-anchor="middle" fill="#999" font-family="sans-serif" font-size="28">
PRODIGI VINYL STICKER - 4" × 4" (1200×1200 px)
</text>
<!-- Guide Legend -->
<g transform="translate(50, 1150)" font-family="sans-serif" font-size="16">
<line x1="0" y1="0" x2="30" y2="0" stroke="#ff0000" stroke-width="2" stroke-dasharray="5,3"/>
<text x="35" y="5" fill="#999">Bleed (extend design here)</text>
<line x1="300" y1="0" x2="330" y2="0" stroke="#ff00ff" stroke-width="2"/>
<text x="335" y="5" fill="#999">Cut line</text>
<line x1="550" y1="0" x2="580" y2="0" stroke="#00ff00" stroke-width="2" stroke-dasharray="8,5"/>
<text x="585" y="5" fill="#999">Safe zone (keep text here)</text>
</g>
<!-- Design Area -->
<g id="design">
<text x="600" y="600" text-anchor="middle" fill="#ccc" font-family="sans-serif" font-size="48">
YOUR DESIGN
</text>
<text x="600" y="660" text-anchor="middle" fill="#999" font-family="sans-serif" font-size="24">
Replace this layer
</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB