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:
commit
a937a0b978
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.* 🍄
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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"
|
||||
|
|
@ -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]")
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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]")
|
||||
|
|
@ -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]")
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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"
|
||||
|
|
@ -0,0 +1 @@
|
|||
"""Tests for mycopunk CLI."""
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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/)
|
||||
|
|
@ -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>` |
|
||||
|
|
@ -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!"
|
||||
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
Loading…
Reference in New Issue