CLI y automatización

Por: Artiko
openpencilcliautomatizaciónci-cdbatchop

CLI y automatización

El CLI op: diseño como código

La herramienta de línea de comandos op es lo que convierte a OpenPencil de un editor visual en una plataforma de diseño programable. Con op puedes automatizar tareas repetitivas, integrar el diseño en pipelines de CI/CD, escribir scripts que generan variantes de diseño, y mucho más.

La filosofía del CLI es simple: todo lo que puedes hacer en la interfaz gráfica, debes poder hacerlo desde la terminal. Y más: el CLI expone operaciones que no tienen una representación visual directa, como el análisis de tokens, el linting de accesibilidad y la generación en batch.

graph LR
    A[Desarrollador] --> B[CLI op]
    B --> C{Operación}
    C --> D[Diseño interactivo<br/>op start]
    C --> E[Batch design<br/>op design]
    C --> F[Exportar código<br/>op export]
    C --> G[Importar Figma<br/>op import:figma]
    C --> H[Linting<br/>op lint]
    C --> I[Tokens<br/>op tokens]

Instalación y configuración del CLI

Si instalaste OpenPencil con Homebrew o Scoop, el CLI op ya está disponible. Para instalarlo de forma standalone:

# Con npm
npm install -g @zseven-w/openpencil

# Con bun (recomendado)
bun install -g @zseven-w/openpencil

# Verificar versión
op --version

# Diagnóstico del entorno
op doctor

Configuración global

El CLI usa un archivo de configuración en ~/.config/openpencil/config.json (o %APPDATA%\openpencil\config.json en Windows):

# Configurar API key de Anthropic
op config set ai.providers.anthropic.apiKey "sk-ant-..."

# Configurar directorio de proyectos por defecto
op config set workspace "~/Diseños"

# Ver configuración actual
op config list

# Reset a valores por defecto
op config reset

Comandos fundamentales

op start — Lanzar la app de escritorio

# Abrir la app sin archivo específico
op start

# Abrir un archivo específico
op start mi-proyecto.op

# Abrir con servidor colaborativo
op start mi-proyecto.op --collab-port 8080

op new — Crear un nuevo documento

# Crear documento vacío
op new mi-proyecto.op

# Crear con template
op new dashboard.op --template dashboard-saas

# Listar templates disponibles
op templates list

op info — Información del documento

# Mostrar metadatos del documento
op info mi-proyecto.op

# Salida ejemplo:
# Document: mi-proyecto.op
# Version: 1.0
# Pages: 3 (Landing, Dashboard, Mobile)
# Nodes: 847
# Components: 23
# Variables: 45
# Last modified: 2026-04-05T10:30:00Z

Diseño batch: op design

El comando op design es la joya del CLI. Permite generar diseños a partir de descripciones en lenguaje natural, ya sea desde un archivo de texto o desde stdin.

Desde un archivo de texto

Crea un archivo landing.txt con la descripción de tu diseño:

# landing.txt
Diseña una landing page para una startup de SaaS de gestión de proyectos.

## Hero Section
- Fondo: gradiente de #1E293B a #0F172A
- Título grande: "Gestiona proyectos con IA"
- Subtítulo: descripción de 2 líneas
- Dos botones: primario "Empezar gratis" y secundario "Ver demo"
- Imagen de mockup a la derecha (placeholder 600x400)

## Features Grid
- 3 columnas, 6 features totales
- Cada feature: icono de Heroicons, título, descripción corta
- Fondo blanco, bordes suaves

## Pricing
- 3 planes: Free, Pro ($29/mes), Enterprise
- Plan Pro destacado como recomendado
- Lista de 5 features por plan
- CTA en cada plan

## Footer
- Links de navegación en 4 columnas
- Copyright y links legales
- Modo oscuro

Ejecuta:

op design @landing.txt --output landing.op

# Con especificación de modelo
op design @landing.txt \
  --model claude-opus-4-5 \
  --level full \
  --output landing.op

# Monitorear el progreso
op design @landing.txt --output landing.op --verbose

Desde stdin (pipes)

# Diseñar desde texto en pipe
echo "Crea un formulario de login con email y contraseña" | op design - --output login.op

# Usar con heredoc
op design - --output componente.op << 'EOF'
Diseña un componente de notificación toast con:
- 4 variantes: success, error, warning, info
- Icono, título y descripción en cada variante
- Botón de cerrar
- Animación de entrada desde la derecha
EOF

# Combinar con herramientas de texto
cat diseños/*.txt | op design - --output resultado.op

Diseño incremental

Para agregar elementos a un documento existente sin reemplazarlo:

# Agregar nuevos componentes a un documento existente
op design @nuevos-componentes.txt \
  --input mi-proyecto.op \
  --output mi-proyecto.op \
  --mode append

Modo dry-run

Antes de ejecutar un diseño batch costoso, puedes hacer un dry-run que estima el costo y tiempo:

op design @landing.txt --dry-run

# Salida:
# Estimated tokens: 45,000
# Estimated cost: $0.18 (Anthropic Opus)
# Estimated time: ~3 minutes
# Pages to create: 1
# Estimated nodes: ~200

Insertar nodos: op insert

El comando op insert permite agregar nodos individuales a un documento mediante JSON:

# Insertar un rectángulo
op insert '{"type":"RECT","x":100,"y":100,"width":200,"height":100}' \
  --file mi-proyecto.op

# Insertar texto
op insert '{
  "type": "TEXT",
  "x": 50,
  "y": 50,
  "content": "Hola mundo",
  "fontSize": 24,
  "fontFamily": "Inter",
  "fills": [{"type": "SOLID", "color": "#1E293B"}]
}' --file mi-proyecto.op

# Insertar frame con children
op insert '{
  "type": "FRAME",
  "x": 0, "y": 0,
  "width": 1440, "height": 900,
  "name": "Landing",
  "autoLayout": {"direction": "vertical", "gap": 0},
  "children": []
}' --file mi-proyecto.op --page "Página 1"

Insertar desde un archivo JSON

Para inserciones complejas, usa un archivo:

// nodo.json
{
  "type": "COMPONENT",
  "name": "Button/Primary",
  "x": 100,
  "y": 100,
  "autoLayout": {
    "direction": "horizontal",
    "gap": 8,
    "padding": {"top": 12, "right": 20, "bottom": 12, "left": 20}
  },
  "fills": [{"type": "SOLID", "color": "#2563EB"}],
  "cornerRadius": 8,
  "children": [
    {
      "type": "TEXT",
      "content": "Enviar",
      "fontSize": 16,
      "fontFamily": "Inter",
      "fontWeight": 600,
      "fills": [{"type": "SOLID", "color": "#FFFFFF"}]
    }
  ]
}
op insert --from nodo.json --file mi-proyecto.op

Exportar diseños: op export

Exportar a React + Tailwind

# Exportar la página activa
op export react --file diseño.op --out ./src/components

# Exportar una página específica
op export react --file diseño.op --page "Dashboard" --out ./src/components

# Exportar un componente específico por nombre
op export react --file diseño.op --component "Button/Primary" --out ./src/components

# Exportar con opciones adicionales
op export react \
  --file diseño.op \
  --out ./src \
  --tailwind-config ./tailwind.config.js \
  --use-shadcn \
  --typescript

El resultado es un componente React con Tailwind:

// Generado por: op export react
import { cn } from '@/lib/utils'

interface ButtonPrimaryProps {
  children: React.ReactNode
  className?: string
  disabled?: boolean
}

export function ButtonPrimary({ children, className, disabled }: ButtonPrimaryProps) {
  return (
    <button
      className={cn(
        'flex items-center gap-2 px-5 py-3 rounded-lg',
        'bg-blue-600 text-white font-semibold text-base',
        'hover:bg-blue-700 transition-colors',
        'disabled:opacity-50 disabled:cursor-not-allowed',
        className
      )}
      disabled={disabled}
    >
      {children}
    </button>
  )
}

Exportar a otros frameworks

# Vue 3 + Tailwind
op export vue --file diseño.op --out ./src/components

# Svelte + Tailwind
op export svelte --file diseño.op --out ./src/lib

# HTML + CSS
op export html --file diseño.op --out ./dist
op export html --file diseño.op --out ./dist --css-variables  # Usa CSS custom props

# Flutter
op export flutter --file diseño.op --out ./lib/widgets

# SwiftUI
op export swiftui --file diseño.op --out ./Sources/Views

# Jetpack Compose
op export compose --file diseño.op --out ./app/src/main/java

# React Native
op export react-native --file diseño.op --out ./src/components

Exportar assets

# Exportar solo los assets (imágenes, iconos SVG)
op export assets --file diseño.op --out ./public/assets --format png
op export assets --file diseño.op --out ./public/assets --format svg
op export assets --file diseño.op --out ./public/assets --format webp --scale 2

Importar Figma: op import:figma

# Importar archivo Figma exportado localmente
op import:figma diseño-figma.fig --output mi-proyecto.op

# Importar desde la API de Figma (requiere token)
op import:figma --figma-token "figd_..." \
  --file-id "XXXXXX" \
  --output mi-proyecto.op

# Importar solo una página específica
op import:figma diseño-figma.fig \
  --page "Mobile" \
  --output mobile.op

# Importar con log detallado de conversión
op import:figma diseño-figma.fig \
  --output mi-proyecto.op \
  --verbose 2>&1 | tee import-log.txt

Análisis y linting: op lint

OpenPencil incluye un linter de diseño que verifica reglas de consistencia, accesibilidad y convenciones:

# Análisis completo
op lint mi-proyecto.op

# Solo accesibilidad (contraste de colores, tamaños de texto)
op lint mi-proyecto.op --rules accessibility

# Solo tokens (verifica que todos los valores estén vinculados a variables)
op lint mi-proyecto.op --rules tokens

# Solo consistencia de espaciado (múltiplos de 4 u 8)
op lint mi-proyecto.op --rules spacing

# Formato JSON para integración con CI
op lint mi-proyecto.op --format json > lint-results.json

Ejemplo de salida:

🔍 OpenPencil Lint Results: mi-proyecto.op
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

⚠️  WARNINGS (3)
  [spacing] Frame "Hero" > Text "Subtitle": margin-top is 13px, expected multiple of 4
  [tokens] Rect "Card BG": fill color #F8F9FA not bound to a variable
  [tokens] Text "Footnote": font-size 13px not in typography scale

❌ ERRORS (1)
  [accessibility] Text "Button Label" on "Button/Primary": contrast ratio 2.8:1 < 4.5:1 (WCAG AA)

📊 Summary: 1 error, 3 warnings, 234 nodes checked

Gestión de tokens: op tokens

# Extraer todos los tokens del documento
op tokens extract mi-proyecto.op

# Exportar tokens como JSON (Style Dictionary)
op tokens export mi-proyecto.op --format style-dictionary --out tokens/

# Exportar tokens como CSS custom properties
op tokens export mi-proyecto.op --format css --out ./src/styles/tokens.css

# Exportar como Tailwind config
op tokens export mi-proyecto.op --format tailwind --out tailwind.tokens.js

# Sincronizar tokens desde un archivo externo hacia el documento
op tokens sync tokens/tokens.json --to mi-proyecto.op

El formato Style Dictionary es compatible con la mayoría de pipelines de tokens modernos:

// tokens/color.json (generado por op tokens export)
{
  "color": {
    "primary": {
      "50":  { "value": "#EFF6FF" },
      "100": { "value": "#DBEAFE" },
      "500": { "value": "#3B82F6" },
      "600": { "value": "#2563EB" },
      "900": { "value": "#1E3A8A" }
    },
    "semantic": {
      "background": { "value": "{color.primary.50}" },
      "text-on-primary": { "value": "#FFFFFF" }
    }
  }
}

Integración en CI/CD

GitHub Actions: verificar tokens en cada PR

# .github/workflows/design-check.yml
name: Design Token Check

on:
  pull_request:
    paths:
      - 'designs/**/*.op'

jobs:
  lint-design:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Install OpenPencil CLI
        run: npm install -g @zseven-w/openpencil
      
      - name: Lint design files
        run: |
          for file in designs/*.op; do
            op lint "$file" --format json > lint-$(basename $file .op).json
          done
      
      - name: Check for errors
        run: |
          for report in lint-*.json; do
            errors=$(jq '.errors | length' "$report")
            if [ "$errors" -gt "0" ]; then
              echo "❌ Errors found in $report"
              jq '.errors[]' "$report"
              exit 1
            fi
          done
          echo "✅ All design files pass lint"
      
      - name: Upload lint reports
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: lint-reports
          path: lint-*.json

GitHub Actions: exportar componentes automáticamente

# .github/workflows/export-components.yml
name: Export Design Components

on:
  push:
    branches: [main]
    paths:
      - 'designs/components.op'

jobs:
  export:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
      
      - name: Install OpenPencil CLI
        run: npm install -g @zseven-w/openpencil
      
      - name: Export React components
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: |
          op export react \
            --file designs/components.op \
            --out src/components/generated \
            --typescript
      
      - name: Commit exported components
        run: |
          git config user.name "OpenPencil Bot"
          git config user.email "[email protected]"
          git add src/components/generated/
          git diff --staged --quiet || git commit -m "chore: sync design components [skip ci]"
          git push

Pipeline de validación de accesibilidad

#!/bin/bash
# scripts/check-design-a11y.sh

DESIGN_FILE="designs/mi-app.op"
THRESHOLD_ERRORS=0

echo "Verificando accesibilidad en $DESIGN_FILE..."

RESULT=$(op lint "$DESIGN_FILE" --rules accessibility --format json)
ERRORS=$(echo "$RESULT" | jq '.errors | length')
WARNINGS=$(echo "$RESULT" | jq '.warnings | length')

echo "Errores: $ERRORS"
echo "Advertencias: $WARNINGS"

if [ "$ERRORS" -gt "$THRESHOLD_ERRORS" ]; then
  echo "❌ Falló la verificación de accesibilidad"
  echo "$RESULT" | jq '.errors[]'
  exit 1
fi

echo "✅ Verificación de accesibilidad aprobada"

Integración con pre-commit hooks

# .husky/pre-commit (si usas Husky)
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

# Verificar archivos .op modificados
CHANGED_OP_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.op$')

if [ -n "$CHANGED_OP_FILES" ]; then
  echo "Verificando archivos de diseño..."
  for file in $CHANGED_OP_FILES; do
    op lint "$file" --format json | python3 -c "
import json, sys
data = json.load(sys.stdin)
if data['errors']:
    print(f'❌ {len(data[\"errors\"])} error(s) en $file')
    sys.exit(1)
print(f'✅ $file OK')
"
  done
fi

Casos de uso avanzados

Generador de variantes de componentes

#!/bin/bash
# scripts/generate-variants.sh
# Genera variantes de color para un componente

BASE_COMPONENT="Button/Primary"
COLORS=("#2563EB" "#7C3AED" "#DC2626" "#16A34A" "#D97706")
NAMES=("Blue" "Purple" "Red" "Green" "Amber")

for i in "${!COLORS[@]}"; do
  COLOR=${COLORS[$i]}
  NAME=${NAMES[$i]}
  
  op design - --input diseño.op --output diseño.op --mode append << EOF
Duplica el componente "$BASE_COMPONENT" y créalo como "Button/$NAME".
Cambia el color de fondo a $COLOR.
Asegura que el color de texto mantenga contraste WCAG AA.
EOF
  
  echo "✅ Generado Button/$NAME ($COLOR)"
done

Sincronización automática de design tokens con el código

#!/bin/bash
# scripts/sync-tokens.sh
# Sincroniza tokens de diseño con el proyecto de código

DESIGN_FILE="./designs/design-system.op"
TOKENS_DIR="./src/styles"

# Exportar tokens en múltiples formatos
op tokens export "$DESIGN_FILE" \
  --format css \
  --out "$TOKENS_DIR/tokens.css"

op tokens export "$DESIGN_FILE" \
  --format json \
  --out "$TOKENS_DIR/tokens.json"

op tokens export "$DESIGN_FILE" \
  --format tailwind \
  --out "./tailwind.tokens.js"

echo "✅ Tokens sincronizados"
echo "Archivos actualizados:"
echo "  - $TOKENS_DIR/tokens.css"
echo "  - $TOKENS_DIR/tokens.json"
echo "  - ./tailwind.tokens.js"

Análisis de uso de tokens

# Ver qué valores no están vinculados a tokens (hardcoded)
op lint mi-proyecto.op --rules tokens --format json | \
  jq '[.warnings[] | select(.rule == "tokens")] | 
      group_by(.value) | 
      map({value: .[0].value, count: length}) | 
      sort_by(.count) | reverse'

# Resultado: ordenado por frecuencia de uso
# [{"value": "#F8F9FA", "count": 23}, ...]
# Estos son buenos candidatos para convertir en variables

Scripting avanzado con la API JSON de .op

Como los archivos .op son JSON, puedes manipularlos con cualquier lenguaje de programación:

// scripts/rename-components.ts
import { readFileSync, writeFileSync } from 'fs'

const doc = JSON.parse(readFileSync('mi-proyecto.op', 'utf-8'))

// Encontrar todos los componentes con nombre antiguo
function renameNodes(nodes: any[], oldPrefix: string, newPrefix: string) {
  for (const node of nodes) {
    if (node.name?.startsWith(oldPrefix)) {
      node.name = node.name.replace(oldPrefix, newPrefix)
    }
    if (node.children) {
      renameNodes(node.children, oldPrefix, newPrefix)
    }
  }
}

for (const page of doc.pages) {
  renameNodes(page.nodes, 'UI/', 'Components/')
}

writeFileSync('mi-proyecto.op', JSON.stringify(doc, null, 2))
console.log('Componentes renombrados')
# scripts/audit_colors.py
import json
from collections import Counter

with open('mi-proyecto.op') as f:
    doc = json.load(f)

colors = []

def extract_colors(node):
    for fill in node.get('fills', []):
        if fill['type'] == 'SOLID':
            colors.append(fill['color'])
    for child in node.get('children', []):
        extract_colors(child)

for page in doc['pages']:
    for node in page['nodes']:
        extract_colors(node)

counter = Counter(colors)
print("Top 10 colores más usados:")
for color, count in counter.most_common(10):
    print(f"  {color}: {count} usos")

Resumen

En este capítulo has aprendido:

En el próximo capítulo aprenderemos a configurar el MCP Server de OpenPencil para trabajar con Claude Code directamente desde el editor.