feat: añadir skills create-tui, init-frontend, init-go-module y utilidades
Nuevas skills para crear TUIs, inicializar frontends React y módulos Go. Incluye binario parallel-executor y utilidades de soporte. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
---
|
||||
name: create-tui
|
||||
description: Scaffoldea una aplicación TUI en Go usando DevFactory (bubbletea) para gestionar scripts, comandos, Makefile y builds de un repositorio
|
||||
argument-hint: [nombre] [--path /ruta/destino]
|
||||
disable-model-invocation: true
|
||||
user-invocable: true
|
||||
allowed-tools: Bash, Read, Write, Edit
|
||||
---
|
||||
|
||||
# create-tui
|
||||
|
||||
Genera un proyecto TUI completo en Go usando los componentes de DevFactory (`tui/` — bubbletea, lipgloss). El TUI resultante permite gestionar un repositorio: ejecutar scripts bash, comandos frecuentes, targets de Makefile y configuraciones de build.
|
||||
|
||||
## Sintaxis
|
||||
|
||||
```bash
|
||||
/create-tui [nombre] [--path /ruta/destino]
|
||||
```
|
||||
|
||||
- `nombre`: nombre del proyecto (kebab-case). Si no se da, se pregunta.
|
||||
- `--path`: directorio destino. Default: directorio actual.
|
||||
|
||||
## Flujo
|
||||
|
||||
### 1. Ejecutar script de setup
|
||||
|
||||
```bash
|
||||
bash "${CLAUDE_SKILL_DIR}/setup-create-tui.sh" [nombre] [path]
|
||||
```
|
||||
|
||||
### 2. Si el script reporta STATUS: CONFIGURED
|
||||
|
||||
Informar al usuario que el proyecto TUI ya existe en esa ruta.
|
||||
|
||||
### 3. Si el script reporta STATUS: READY
|
||||
|
||||
Mostrar resumen:
|
||||
- Estructura creada (app/, views/, config/)
|
||||
- Cómo ejecutar: `make run` o `go run .`
|
||||
- Cómo compilar: `make build`
|
||||
- Cómo instalar: `make install`
|
||||
- Navegación: flechas para moverse, Enter para interactuar, Esc/0 para volver, Esc desde menú principal para salir
|
||||
|
||||
### 4. Si el script reporta STATUS: ERROR
|
||||
|
||||
Mostrar el error y sugerir corrección.
|
||||
|
||||
## Convenciones
|
||||
|
||||
- Usa DevFactory como dependencia via `go.work` (componentes tui/, shell/, core/)
|
||||
- Patrón Elm Architecture de bubbletea (Model → Update → View)
|
||||
- `Result[T]` del core de DevFactory para manejo de errores
|
||||
- Ejecución async de comandos via `tea.Cmd`
|
||||
- Navegación: flechas + Enter + Esc/0 en todas las vistas
|
||||
- El TUI opera sobre un directorio target (default: `.`, configurable por argumento)
|
||||
+1481
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,62 @@
|
||||
---
|
||||
name: init-frontend
|
||||
description: Inicializa proyecto frontend (React/Vite) o desktop (Wails) con Frontend_Library
|
||||
disable-model-invocation: true
|
||||
user-invocable: true
|
||||
allowed-tools: Bash, Read, Write, Edit
|
||||
---
|
||||
|
||||
# init-frontend
|
||||
|
||||
Inicializa un proyecto frontend (webapp React/Vite) o desktop (Wails + Go + React). Coherente con Frontend_Library y el stack del frontend-lib/build-wails agents.
|
||||
|
||||
## Sintaxis
|
||||
|
||||
```bash
|
||||
/init-frontend [nombre] [--wails] [--path /ruta/destino]
|
||||
```
|
||||
|
||||
- `nombre`: nombre del proyecto (kebab-case). Si no se da, se pregunta.
|
||||
- `--wails`: modo desktop con Wails (Go backend + React frontend). Sin flag = webapp pura.
|
||||
- `--path`: directorio destino. Default: directorio actual.
|
||||
|
||||
## Flujo
|
||||
|
||||
### 1. Ejecutar script de setup
|
||||
|
||||
```bash
|
||||
bash "${CLAUDE_SKILL_DIR}/setup-frontend.sh" [nombre] [--wails] [path]
|
||||
```
|
||||
|
||||
### 2. Si el script reporta STATUS: CONFIGURED
|
||||
|
||||
Informar al usuario que el proyecto ya existe.
|
||||
|
||||
### 3. Si el script reporta STATUS: READY
|
||||
|
||||
Mostrar resumen según modo:
|
||||
|
||||
**Webapp:**
|
||||
- `pnpm dev` para desarrollo
|
||||
- `pnpm build` para producción
|
||||
- Frontend_Library linkeada via pnpm
|
||||
|
||||
**Wails:**
|
||||
- `make dev` para desarrollo con hot reload
|
||||
- `make build` para compilar
|
||||
- Frontend_Library + DevFactory integrados
|
||||
- Bindings Go→TS auto-generados
|
||||
|
||||
### 4. Si el script reporta STATUS: ERROR
|
||||
|
||||
Mostrar el error y sugerir corrección.
|
||||
|
||||
## Convenciones
|
||||
|
||||
- pnpm exclusivamente (no npm ni yarn)
|
||||
- React 19 + TypeScript + Vite + Tailwind CSS 4
|
||||
- @anthropic/frontend-lib via pnpm link
|
||||
- Temas OKLCH con semantic tokens
|
||||
- Phosphor Icons
|
||||
- Vite dedupe obligatorio para react/react-dom
|
||||
- En modo Wails: go.work con DevFactory, patrón pure core / impure shell
|
||||
+438
@@ -0,0 +1,438 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# =============================================================================
|
||||
# setup-frontend.sh — Inicializa proyecto React/Vite o Wails desktop
|
||||
# Coherente con Frontend_Library + DevFactory + build-wails agent
|
||||
# =============================================================================
|
||||
|
||||
# --- Colores ---
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
||||
log_ok() { echo -e "${GREEN}[OK]${NC} $1"; }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
log_step() { echo -e "${CYAN}[STEP]${NC} $1"; }
|
||||
|
||||
# --- Parámetros ---
|
||||
PROJECT_NAME=""
|
||||
WAILS_MODE=false
|
||||
TARGET_PATH="."
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--wails) WAILS_MODE=true; shift ;;
|
||||
--path) TARGET_PATH="$2"; shift 2 ;;
|
||||
-*) log_error "Flag desconocido: $1"; echo "STATUS: ERROR"; exit 1 ;;
|
||||
*) PROJECT_NAME="$1"; shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# --- Rutas de librerías ---
|
||||
FRONTEND_LIB="$HOME/.local_agentes/frontend/frontend"
|
||||
DEVFACTORY_PATH="$HOME/.local_agentes/backend"
|
||||
TEMPLATES_DIR="$HOME/.local_agentes/frontend/templates/base"
|
||||
WAILS_TEMPLATES="$HOME/.claude/agents/build-wails/templates"
|
||||
|
||||
# --- Validar nombre ---
|
||||
if [[ -z "$PROJECT_NAME" ]]; then
|
||||
log_error "Uso: setup-frontend.sh <nombre> [--wails] [--path /ruta]"
|
||||
echo "STATUS: ERROR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Normalizar
|
||||
PROJECT_NAME=$(echo "$PROJECT_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g' | sed 's/--*/-/g' | sed 's/^-\|-$//g')
|
||||
PROJECT_DIR="$TARGET_PATH/$PROJECT_NAME"
|
||||
|
||||
# --- Check estado existente ---
|
||||
if [[ -f "$PROJECT_DIR/package.json" ]] || [[ -f "$PROJECT_DIR/wails.json" ]]; then
|
||||
log_warn "El proyecto $PROJECT_NAME ya existe en $PROJECT_DIR"
|
||||
echo "STATUS: CONFIGURED"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- Verificar dependencias ---
|
||||
log_step "Verificando dependencias..."
|
||||
|
||||
if ! command -v pnpm &>/dev/null; then
|
||||
log_error "pnpm no está instalado. Instala con: npm install -g pnpm"
|
||||
echo "STATUS: ERROR"
|
||||
exit 1
|
||||
fi
|
||||
log_ok "pnpm $(pnpm --version) encontrado"
|
||||
|
||||
if ! command -v node &>/dev/null; then
|
||||
log_error "Node.js no encontrado"
|
||||
echo "STATUS: ERROR"
|
||||
exit 1
|
||||
fi
|
||||
log_ok "Node $(node --version) encontrado"
|
||||
|
||||
if [[ "$WAILS_MODE" == true ]]; then
|
||||
if ! command -v wails &>/dev/null; then
|
||||
log_error "Wails no está instalado. Instala con: go install github.com/wailsapp/wails/v2/cmd/wails@latest"
|
||||
echo "STATUS: ERROR"
|
||||
exit 1
|
||||
fi
|
||||
log_ok "Wails $(wails version 2>/dev/null | head -1 || echo 'v2.x') encontrado"
|
||||
|
||||
if ! command -v go &>/dev/null; then
|
||||
log_error "Go no encontrado (requerido para Wails)"
|
||||
echo "STATUS: ERROR"
|
||||
exit 1
|
||||
fi
|
||||
log_ok "Go $(go version | grep -oP '\d+\.\d+' | head -1) encontrado"
|
||||
fi
|
||||
|
||||
if [[ ! -d "$FRONTEND_LIB" ]]; then
|
||||
log_warn "Frontend_Library no encontrada en $FRONTEND_LIB — se creará sin link"
|
||||
HAS_FRONTEND_LIB=false
|
||||
else
|
||||
log_ok "Frontend_Library encontrada"
|
||||
HAS_FRONTEND_LIB=true
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# MODO WAILS — Desktop app (Go + React)
|
||||
# ============================================================================
|
||||
if [[ "$WAILS_MODE" == true ]]; then
|
||||
log_step "Creando proyecto Wails '$PROJECT_NAME'..."
|
||||
|
||||
# Usar wails init con template react-ts
|
||||
wails init -n "$PROJECT_NAME" -t react-ts -d "$TARGET_PATH" 2>/dev/null
|
||||
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
# --- go.work con DevFactory ---
|
||||
if [[ -d "$DEVFACTORY_PATH" ]]; then
|
||||
log_step "Configurando go.work con DevFactory..."
|
||||
cat > go.work << EOF
|
||||
go 1.22
|
||||
|
||||
use (
|
||||
.
|
||||
$DEVFACTORY_PATH
|
||||
)
|
||||
EOF
|
||||
log_ok "DevFactory enlazado via go.work"
|
||||
fi
|
||||
|
||||
# --- Configurar frontend con pnpm ---
|
||||
log_step "Configurando frontend con pnpm..."
|
||||
cd frontend
|
||||
|
||||
# Reemplazar npm por pnpm en wails.json
|
||||
cd ..
|
||||
if [[ -f "wails.json" ]]; then
|
||||
sed -i 's/"npm install"/"pnpm install"/g' wails.json
|
||||
sed -i 's/"npm run dev"/"pnpm dev"/g' wails.json
|
||||
sed -i 's/"npm run build"/"pnpm build"/g' wails.json
|
||||
fi
|
||||
cd frontend
|
||||
|
||||
# Instalar con pnpm
|
||||
rm -f package-lock.json 2>/dev/null || true
|
||||
pnpm install
|
||||
|
||||
# --- Linkear Frontend_Library ---
|
||||
if [[ "$HAS_FRONTEND_LIB" == true ]]; then
|
||||
log_step "Linkeando Frontend_Library..."
|
||||
pnpm add "@anthropic/frontend-lib@link:$FRONTEND_LIB"
|
||||
log_ok "@anthropic/frontend-lib linkeada"
|
||||
fi
|
||||
|
||||
# --- Instalar Tailwind CSS 4 ---
|
||||
log_step "Instalando Tailwind CSS 4..."
|
||||
pnpm add -D tailwindcss @tailwindcss/vite
|
||||
|
||||
# --- Configurar vite.config.ts con dedupe y tailwind ---
|
||||
log_step "Configurando Vite (dedupe + tailwind)..."
|
||||
cat > vite.config.ts << 'VEOF'
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { resolve } from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, './src'),
|
||||
'@wails': resolve(__dirname, './wailsjs'),
|
||||
},
|
||||
dedupe: ['react', 'react-dom'],
|
||||
},
|
||||
})
|
||||
VEOF
|
||||
|
||||
# --- CSS base con Tailwind ---
|
||||
cat > src/style.css << 'CSSEOF'
|
||||
@import "tailwindcss";
|
||||
CSSEOF
|
||||
|
||||
# --- Instalar Phosphor Icons ---
|
||||
pnpm add @phosphor-icons/react
|
||||
|
||||
cd ..
|
||||
|
||||
# --- Makefile (basado en build-wails agent) ---
|
||||
log_step "Generando Makefile..."
|
||||
cat > Makefile << 'MKEOF'
|
||||
.PHONY: dev dev-debug build build-prod build-linux build-windows build-all clean generate doctor
|
||||
|
||||
APP_NAME := $(shell basename $(CURDIR))
|
||||
|
||||
## dev: Desarrollo con hot reload
|
||||
dev:
|
||||
wails dev
|
||||
|
||||
## dev-debug: Desarrollo con DevTools
|
||||
dev-debug:
|
||||
wails dev -devtools
|
||||
|
||||
## build: Build para plataforma actual
|
||||
build:
|
||||
wails build
|
||||
|
||||
## build-prod: Build optimizado para producción
|
||||
build-prod:
|
||||
wails build -clean -trimpath -ldflags="-s -w"
|
||||
|
||||
## build-linux: Build para Linux AMD64
|
||||
build-linux:
|
||||
wails build -platform linux/amd64
|
||||
|
||||
## build-windows: Cross-compile para Windows
|
||||
build-windows:
|
||||
wails build -platform windows/amd64
|
||||
|
||||
## build-all: Linux + Windows
|
||||
build-all: build-linux build-windows
|
||||
|
||||
## generate: Regenerar bindings TypeScript
|
||||
generate:
|
||||
wails generate module
|
||||
|
||||
## clean: Limpiar artefactos
|
||||
clean:
|
||||
rm -rf build/bin frontend/dist
|
||||
|
||||
## doctor: Verificar instalación
|
||||
doctor:
|
||||
wails doctor
|
||||
|
||||
## help: Muestra esta ayuda
|
||||
help:
|
||||
@grep -E '^## ' Makefile | sed 's/## //' | column -t -s ':'
|
||||
MKEOF
|
||||
|
||||
# --- .gitignore ---
|
||||
cat >> .gitignore << 'EOF'
|
||||
node_modules/
|
||||
frontend/dist/
|
||||
build/bin/
|
||||
*.exe
|
||||
EOF
|
||||
|
||||
# --- Resumen Wails ---
|
||||
echo ""
|
||||
log_ok "Proyecto Wails '$PROJECT_NAME' creado en $PROJECT_DIR"
|
||||
echo ""
|
||||
echo -e "${CYAN}Estructura:${NC}"
|
||||
echo " $PROJECT_NAME/"
|
||||
echo " ├── main.go, app.go — Backend Go"
|
||||
echo " ├── go.work — Enlace a DevFactory"
|
||||
echo " ├── frontend/ — React + TypeScript + Vite"
|
||||
echo " │ ├── src/ — Componentes React"
|
||||
echo " │ └── wailsjs/ — Bindings auto-generados"
|
||||
echo " ├── Makefile — dev, build, build-all"
|
||||
echo " └── wails.json — Configuración Wails"
|
||||
echo ""
|
||||
echo -e "${CYAN}Comandos:${NC}"
|
||||
echo " make dev — Desarrollo con hot reload"
|
||||
echo " make build — Compilar app desktop"
|
||||
echo " make build-all — Linux + Windows"
|
||||
echo " make generate — Regenerar bindings TS"
|
||||
echo ""
|
||||
if [[ "$HAS_FRONTEND_LIB" == true ]]; then
|
||||
echo -e "${CYAN}Frontend_Library:${NC}"
|
||||
echo " import { Button, Card } from '@anthropic/frontend-lib'"
|
||||
echo " import { useTheme } from '@anthropic/frontend-lib/hooks'"
|
||||
echo ""
|
||||
fi
|
||||
echo "STATUS: READY"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# MODO WEBAPP — React + Vite (sin Wails)
|
||||
# ============================================================================
|
||||
log_step "Creando proyecto webapp '$PROJECT_NAME'..."
|
||||
|
||||
mkdir -p "$PROJECT_DIR"
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
# --- Usar template de Frontend_Library si existe ---
|
||||
if [[ -d "$TEMPLATES_DIR" ]]; then
|
||||
log_step "Usando template de Frontend_Library..."
|
||||
cp -r "$TEMPLATES_DIR"/* .
|
||||
cp -r "$TEMPLATES_DIR"/.[!.]* . 2>/dev/null || true
|
||||
else
|
||||
log_step "Creando desde cero con Vite..."
|
||||
|
||||
# package.json
|
||||
cat > package.json << PKGEOF
|
||||
{
|
||||
"name": "$PROJECT_NAME",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
}
|
||||
}
|
||||
PKGEOF
|
||||
|
||||
# Instalar dependencias base
|
||||
pnpm add react react-dom
|
||||
pnpm add -D typescript @types/react @types/react-dom
|
||||
pnpm add -D vite @vitejs/plugin-react
|
||||
pnpm add -D tailwindcss @tailwindcss/vite
|
||||
|
||||
# tsconfig.json
|
||||
cat > tsconfig.json << 'TSEOF'
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2023",
|
||||
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"paths": { "@/*": ["./src/*"] },
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
TSEOF
|
||||
|
||||
# vite.config.ts
|
||||
cat > vite.config.ts << 'VEOF'
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { resolve } from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: { '@': resolve(__dirname, './src') },
|
||||
dedupe: ['react', 'react-dom'],
|
||||
},
|
||||
})
|
||||
VEOF
|
||||
|
||||
# index.html
|
||||
cat > index.html << HTMLEOF
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>$PROJECT_NAME</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
HTMLEOF
|
||||
|
||||
# src/
|
||||
mkdir -p src
|
||||
|
||||
cat > src/main.tsx << 'TSXEOF'
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import App from './App'
|
||||
import './app.css'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
TSXEOF
|
||||
|
||||
cat > src/App.tsx << 'TSXEOF'
|
||||
function App() {
|
||||
return (
|
||||
<div className="min-h-screen bg-surface text-foreground flex items-center justify-center">
|
||||
<h1 className="text-3xl font-bold">Ready</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
TSXEOF
|
||||
|
||||
cat > src/app.css << 'CSSEOF'
|
||||
@import "tailwindcss";
|
||||
CSSEOF
|
||||
fi
|
||||
|
||||
# --- Linkear Frontend_Library ---
|
||||
if [[ "$HAS_FRONTEND_LIB" == true ]]; then
|
||||
log_step "Linkeando Frontend_Library..."
|
||||
pnpm add "@anthropic/frontend-lib@link:$FRONTEND_LIB"
|
||||
log_ok "@anthropic/frontend-lib linkeada"
|
||||
fi
|
||||
|
||||
# --- Phosphor Icons ---
|
||||
pnpm add @phosphor-icons/react
|
||||
|
||||
# --- .gitignore ---
|
||||
cat > .gitignore << 'EOF'
|
||||
node_modules/
|
||||
dist/
|
||||
.vite/
|
||||
*.local
|
||||
EOF
|
||||
|
||||
# --- Resumen Webapp ---
|
||||
echo ""
|
||||
log_ok "Proyecto webapp '$PROJECT_NAME' creado en $PROJECT_DIR"
|
||||
echo ""
|
||||
echo -e "${CYAN}Estructura:${NC}"
|
||||
echo " $PROJECT_NAME/"
|
||||
echo " ├── src/"
|
||||
echo " │ ├── App.tsx — Componente principal"
|
||||
echo " │ ├── main.tsx — Entry point"
|
||||
echo " │ └── app.css — Tailwind CSS"
|
||||
echo " ├── vite.config.ts — Vite + Tailwind + dedupe"
|
||||
echo " ├── tsconfig.json — TypeScript strict"
|
||||
echo " └── package.json — pnpm"
|
||||
echo ""
|
||||
echo -e "${CYAN}Comandos:${NC}"
|
||||
echo " pnpm dev — Servidor de desarrollo"
|
||||
echo " pnpm build — Build de producción"
|
||||
echo " pnpm preview — Preview del build"
|
||||
echo ""
|
||||
if [[ "$HAS_FRONTEND_LIB" == true ]]; then
|
||||
echo -e "${CYAN}Frontend_Library:${NC}"
|
||||
echo " import { Button, Card } from '@anthropic/frontend-lib'"
|
||||
echo " import { useTheme } from '@anthropic/frontend-lib/hooks'"
|
||||
echo ""
|
||||
fi
|
||||
echo "STATUS: READY"
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
name: init-go-module
|
||||
description: Inicializa un módulo Go funcional con bindings Python (CGO c-shared + ctypes)
|
||||
disable-model-invocation: true
|
||||
user-invocable: true
|
||||
allowed-tools: Bash, Read, Write, Edit
|
||||
---
|
||||
|
||||
# init-go-module
|
||||
|
||||
Inicializa un módulo Go con arquitectura funcional (pure core / impure shell) y bindings Python automáticos via CGO c-shared + ctypes. Coherente con DevFactory y el stack del backend-lib agent.
|
||||
|
||||
## Sintaxis
|
||||
|
||||
```bash
|
||||
/init-go-module [nombre] [--path /ruta/destino]
|
||||
```
|
||||
|
||||
- `nombre`: nombre del módulo (kebab-case). Si no se da, se pregunta.
|
||||
- `--path`: directorio destino. Default: directorio actual.
|
||||
|
||||
## Flujo
|
||||
|
||||
### 1. Ejecutar script de setup
|
||||
|
||||
```bash
|
||||
bash "${CLAUDE_SKILL_DIR}/setup-go-module.sh" [nombre] [path]
|
||||
```
|
||||
|
||||
### 2. Si el script reporta STATUS: CONFIGURED
|
||||
|
||||
Informar al usuario que el módulo ya está configurado.
|
||||
|
||||
### 3. Si el script reporta STATUS: READY
|
||||
|
||||
Mostrar resumen:
|
||||
- Estructura creada
|
||||
- Cómo compilar: `make build`
|
||||
- Cómo generar bindings Python: `make python`
|
||||
- Cómo testear: `make test`
|
||||
- Cómo usar desde Python: `from bindings.modulo import *`
|
||||
|
||||
### 4. Si el script reporta STATUS: ERROR
|
||||
|
||||
Mostrar el error y sugerir corrección.
|
||||
|
||||
## Convenciones
|
||||
|
||||
- Usa DevFactory como dependencia via `go.work` (igual que build-wails)
|
||||
- Patrón pure core / impure shell de DevFactory
|
||||
- `Result[T]` y `Option[T]` del core de DevFactory
|
||||
- Funciones exportadas a Python son thin wrappers en `export/`
|
||||
- El wrapper Python se auto-genera desde los `//export` comments
|
||||
+466
@@ -0,0 +1,466 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# =============================================================================
|
||||
# setup-go-module.sh — Inicializa módulo Go funcional con bindings Python
|
||||
# Coherente con DevFactory (pure core / impure shell) + CGO c-shared + ctypes
|
||||
# =============================================================================
|
||||
|
||||
# --- Colores ---
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
||||
log_ok() { echo -e "${GREEN}[OK]${NC} $1"; }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
log_step() { echo -e "${CYAN}[STEP]${NC} $1"; }
|
||||
|
||||
# --- Parámetros ---
|
||||
MODULE_NAME="${1:-}"
|
||||
TARGET_PATH="${2:-.}"
|
||||
DEVFACTORY_PATH="$HOME/.local_agentes/backend"
|
||||
DEVFACTORY_MODULE="github.com/lucasdataproyects/devfactory"
|
||||
|
||||
# --- Validar nombre ---
|
||||
if [[ -z "$MODULE_NAME" ]]; then
|
||||
log_error "Uso: setup-go-module.sh <nombre> [path]"
|
||||
echo "STATUS: ERROR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Normalizar nombre a kebab-case
|
||||
MODULE_NAME=$(echo "$MODULE_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g' | sed 's/--*/-/g' | sed 's/^-\|-$//g')
|
||||
PROJECT_DIR="$TARGET_PATH/$MODULE_NAME"
|
||||
|
||||
# --- Check estado existente ---
|
||||
if [[ -f "$PROJECT_DIR/go.mod" ]]; then
|
||||
log_warn "El módulo $MODULE_NAME ya existe en $PROJECT_DIR"
|
||||
if [[ -f "$PROJECT_DIR/export/exports.go" ]]; then
|
||||
log_ok "Bindings Python ya configurados"
|
||||
else
|
||||
log_warn "Falta directorio export/ — ejecuta de nuevo para completar"
|
||||
fi
|
||||
echo "STATUS: CONFIGURED"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- Verificar dependencias ---
|
||||
log_step "Verificando dependencias..."
|
||||
|
||||
if ! command -v go &>/dev/null; then
|
||||
log_error "Go no está instalado. Instala Go 1.22+"
|
||||
echo "STATUS: ERROR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
GO_VERSION=$(go version | grep -oP '\d+\.\d+' | head -1)
|
||||
log_ok "Go $GO_VERSION encontrado"
|
||||
|
||||
if ! command -v python3 &>/dev/null; then
|
||||
log_warn "Python3 no encontrado — los bindings se generarán pero no se podrán testear"
|
||||
fi
|
||||
|
||||
if [[ ! -d "$DEVFACTORY_PATH" ]]; then
|
||||
log_warn "DevFactory no encontrado en $DEVFACTORY_PATH — se creará go.mod sin go.work"
|
||||
fi
|
||||
|
||||
# --- Crear estructura ---
|
||||
log_step "Creando estructura del módulo '$MODULE_NAME'..."
|
||||
|
||||
mkdir -p "$PROJECT_DIR"/{core,shell,export,python/bindings,cmd,internal}
|
||||
|
||||
# --- go.mod ---
|
||||
log_step "Generando go.mod..."
|
||||
cat > "$PROJECT_DIR/go.mod" << EOF
|
||||
module github.com/lucasdataproyects/$MODULE_NAME
|
||||
|
||||
go 1.22
|
||||
|
||||
require $DEVFACTORY_MODULE v0.0.0
|
||||
EOF
|
||||
|
||||
# --- go.work (si DevFactory existe localmente) ---
|
||||
if [[ -d "$DEVFACTORY_PATH" ]]; then
|
||||
log_step "Generando go.work con DevFactory local..."
|
||||
cat > "$PROJECT_DIR/go.work" << EOF
|
||||
go 1.22
|
||||
|
||||
use (
|
||||
.
|
||||
$DEVFACTORY_PATH
|
||||
)
|
||||
EOF
|
||||
log_ok "go.work enlazado a DevFactory"
|
||||
fi
|
||||
|
||||
# --- core/transform.go — Funciones puras de ejemplo ---
|
||||
log_step "Generando core/ (funciones puras)..."
|
||||
cat > "$PROJECT_DIR/core/transform.go" << 'GOEOF'
|
||||
// Package core contiene funciones puras sin side effects.
|
||||
// Todas las funciones son deterministas y composables.
|
||||
package core
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"errors"
|
||||
|
||||
df "github.com/lucasdataproyects/devfactory/core"
|
||||
)
|
||||
|
||||
// ToUpper transforma texto a mayúsculas (función pura).
|
||||
func ToUpper(s string) string {
|
||||
return strings.ToUpper(s)
|
||||
}
|
||||
|
||||
// ProcessItems aplica una transformación a cada elemento usando MapSlice de DevFactory.
|
||||
func ProcessItems(items []string, transform func(string) string) []string {
|
||||
return df.MapSlice(items, transform)
|
||||
}
|
||||
|
||||
// FilterNonEmpty filtra elementos vacíos usando FilterSlice de DevFactory.
|
||||
func FilterNonEmpty(items []string) []string {
|
||||
return df.FilterSlice(items, func(s string) bool {
|
||||
return len(strings.TrimSpace(s)) > 0
|
||||
})
|
||||
}
|
||||
|
||||
// SafeDivide retorna Result[float64] para evitar panic en división por cero.
|
||||
func SafeDivide(a, b float64) df.Result[float64] {
|
||||
if b == 0 {
|
||||
return df.Err[float64](errors.New("division by zero"))
|
||||
}
|
||||
return df.Ok(a / b)
|
||||
}
|
||||
GOEOF
|
||||
|
||||
# --- core/types.go — Tipos exportables a Python ---
|
||||
cat > "$PROJECT_DIR/core/types.go" << 'GOEOF'
|
||||
package core
|
||||
|
||||
// DataPoint representa un punto de datos exportable a Python.
|
||||
// Los campos usan tipos C-compatible para facilitar el binding.
|
||||
type DataPoint struct {
|
||||
Label string
|
||||
Value float64
|
||||
}
|
||||
|
||||
// Summary es el resultado de un procesamiento, exportable a Python.
|
||||
type Summary struct {
|
||||
Count int
|
||||
Total float64
|
||||
Items []string
|
||||
}
|
||||
GOEOF
|
||||
|
||||
# --- shell/io.go — Operaciones I/O con Result[T] ---
|
||||
log_step "Generando shell/ (operaciones I/O)..."
|
||||
cat > "$PROJECT_DIR/shell/io.go" << 'GOEOF'
|
||||
// Package shell contiene operaciones con side effects, wrapeadas en Result[T].
|
||||
package shell
|
||||
|
||||
import (
|
||||
df "github.com/lucasdataproyects/devfactory/core"
|
||||
"github.com/lucasdataproyects/devfactory/shell"
|
||||
)
|
||||
|
||||
// ReadDataFile lee un archivo y retorna su contenido como Result.
|
||||
func ReadDataFile(path string) df.Result[string] {
|
||||
return shell.ReadString(path)
|
||||
}
|
||||
|
||||
// WriteResult escribe un resultado a archivo.
|
||||
func WriteResult(path string, content string) df.Result[struct{}] {
|
||||
return shell.WriteString(path, content)
|
||||
}
|
||||
GOEOF
|
||||
|
||||
# --- export/exports.go — Funciones exportadas via CGO ---
|
||||
log_step "Generando export/ (bindings CGO)..."
|
||||
cat > "$PROJECT_DIR/export/exports.go" << GOEOF
|
||||
// Package main exporta funciones Go como C shared library.
|
||||
// Cada función con //export se expone como símbolo C callable desde Python.
|
||||
package main
|
||||
|
||||
import "C"
|
||||
import (
|
||||
"encoding/json"
|
||||
"unsafe"
|
||||
|
||||
"github.com/lucasdataproyects/$MODULE_NAME/core"
|
||||
)
|
||||
|
||||
//export GoToUpper
|
||||
func GoToUpper(input *C.char) *C.char {
|
||||
result := core.ToUpper(C.GoString(input))
|
||||
return C.CString(result)
|
||||
}
|
||||
|
||||
//export GoProcessItems
|
||||
func GoProcessItems(jsonInput *C.char) *C.char {
|
||||
var items []string
|
||||
if err := json.Unmarshal([]byte(C.GoString(jsonInput)), &items); err != nil {
|
||||
return C.CString("[]")
|
||||
}
|
||||
result := core.ProcessItems(items, core.ToUpper)
|
||||
out, _ := json.Marshal(result)
|
||||
return C.CString(string(out))
|
||||
}
|
||||
|
||||
//export GoFilterNonEmpty
|
||||
func GoFilterNonEmpty(jsonInput *C.char) *C.char {
|
||||
var items []string
|
||||
if err := json.Unmarshal([]byte(C.GoString(jsonInput)), &items); err != nil {
|
||||
return C.CString("[]")
|
||||
}
|
||||
result := core.FilterNonEmpty(items)
|
||||
out, _ := json.Marshal(result)
|
||||
return C.CString(string(out))
|
||||
}
|
||||
|
||||
//export GoSafeDivide
|
||||
func GoSafeDivide(a, b C.double) *C.char {
|
||||
result := core.SafeDivide(float64(a), float64(b))
|
||||
if result.IsErr() {
|
||||
return C.CString(`{"error":"` + result.Error().Error() + `"}`)
|
||||
}
|
||||
out, _ := json.Marshal(map[string]float64{"value": result.Unwrap()})
|
||||
return C.CString(string(out))
|
||||
}
|
||||
|
||||
//export GoFree
|
||||
func GoFree(ptr *C.char) {
|
||||
C.free(unsafe.Pointer(ptr))
|
||||
}
|
||||
|
||||
func main() {}
|
||||
GOEOF
|
||||
|
||||
# --- python/bindings/__init__.py — Wrapper ctypes auto-generado ---
|
||||
log_step "Generando python/bindings/ (ctypes wrapper)..."
|
||||
|
||||
# Nombre de la shared library según OS
|
||||
SO_NAME="lib${MODULE_NAME}.so"
|
||||
|
||||
cat > "$PROJECT_DIR/python/bindings/__init__.py" << PYEOF
|
||||
"""
|
||||
Auto-generated Python bindings for $MODULE_NAME.
|
||||
Uses ctypes to call Go functions compiled as C shared library.
|
||||
|
||||
Usage:
|
||||
from bindings import to_upper, process_items, filter_non_empty, safe_divide
|
||||
"""
|
||||
import ctypes
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Localizar la shared library
|
||||
_LIB_DIR = Path(__file__).parent.parent.parent / "build"
|
||||
_LIB_NAME = "$SO_NAME"
|
||||
_LIB_PATH = _LIB_DIR / _LIB_NAME
|
||||
|
||||
if not _LIB_PATH.exists():
|
||||
raise FileNotFoundError(
|
||||
f"Shared library not found at {_LIB_PATH}. "
|
||||
f"Run 'make build' in the project root first."
|
||||
)
|
||||
|
||||
_lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
|
||||
# --- Configurar tipos de retorno ---
|
||||
_lib.GoToUpper.argtypes = [ctypes.c_char_p]
|
||||
_lib.GoToUpper.restype = ctypes.c_char_p
|
||||
|
||||
_lib.GoProcessItems.argtypes = [ctypes.c_char_p]
|
||||
_lib.GoProcessItems.restype = ctypes.c_char_p
|
||||
|
||||
_lib.GoFilterNonEmpty.argtypes = [ctypes.c_char_p]
|
||||
_lib.GoFilterNonEmpty.restype = ctypes.c_char_p
|
||||
|
||||
_lib.GoSafeDivide.argtypes = [ctypes.c_double, ctypes.c_double]
|
||||
_lib.GoSafeDivide.restype = ctypes.c_char_p
|
||||
|
||||
_lib.GoFree.argtypes = [ctypes.c_char_p]
|
||||
_lib.GoFree.restype = None
|
||||
|
||||
|
||||
def to_upper(text: str) -> str:
|
||||
"""Convert text to uppercase using Go core."""
|
||||
result = _lib.GoToUpper(text.encode("utf-8"))
|
||||
return result.decode("utf-8")
|
||||
|
||||
|
||||
def process_items(items: list[str]) -> list[str]:
|
||||
"""Process items through Go pipeline (ToUpper transformation)."""
|
||||
input_json = json.dumps(items).encode("utf-8")
|
||||
result = _lib.GoProcessItems(input_json)
|
||||
return json.loads(result.decode("utf-8"))
|
||||
|
||||
|
||||
def filter_non_empty(items: list[str]) -> list[str]:
|
||||
"""Filter empty strings using Go core."""
|
||||
input_json = json.dumps(items).encode("utf-8")
|
||||
result = _lib.GoFilterNonEmpty(input_json)
|
||||
return json.loads(result.decode("utf-8"))
|
||||
|
||||
|
||||
def safe_divide(a: float, b: float) -> float:
|
||||
"""Safe division using Go Result type. Raises ValueError on division by zero."""
|
||||
result = _lib.GoSafeDivide(ctypes.c_double(a), ctypes.c_double(b))
|
||||
data = json.loads(result.decode("utf-8"))
|
||||
if "error" in data:
|
||||
raise ValueError(data["error"])
|
||||
return data["value"]
|
||||
PYEOF
|
||||
|
||||
# --- python/example.py ---
|
||||
cat > "$PROJECT_DIR/python/example.py" << PYEOF
|
||||
"""Example usage of $MODULE_NAME Go bindings from Python."""
|
||||
from bindings import to_upper, process_items, filter_non_empty, safe_divide
|
||||
|
||||
# String transformation
|
||||
print(to_upper("hello from go")) # HELLO FROM GO
|
||||
|
||||
# Batch processing via Go's MapSlice
|
||||
items = ["hello", "world", "from", "go"]
|
||||
print(process_items(items)) # ["HELLO", "WORLD", "FROM", "GO"]
|
||||
|
||||
# Filtering via Go's FilterSlice
|
||||
mixed = ["hello", "", "world", " ", "go"]
|
||||
print(filter_non_empty(mixed)) # ["hello", "world", "go"]
|
||||
|
||||
# Safe division with Result[T] error handling
|
||||
print(safe_divide(10.0, 3.0)) # 3.333...
|
||||
try:
|
||||
safe_divide(10.0, 0.0)
|
||||
except ValueError as e:
|
||||
print(f"Caught: {e}") # Caught: division by zero
|
||||
PYEOF
|
||||
|
||||
# --- core/transform_test.go ---
|
||||
log_step "Generando tests..."
|
||||
cat > "$PROJECT_DIR/core/transform_test.go" << 'GOEOF'
|
||||
package core
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestToUpper(t *testing.T) {
|
||||
if got := ToUpper("hello"); got != "HELLO" {
|
||||
t.Errorf("ToUpper(\"hello\") = %q, want %q", got, "HELLO")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterNonEmpty(t *testing.T) {
|
||||
items := []string{"hello", "", "world", " ", "go"}
|
||||
result := FilterNonEmpty(items)
|
||||
if len(result) != 3 {
|
||||
t.Errorf("FilterNonEmpty got %d items, want 3", len(result))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSafeDivide(t *testing.T) {
|
||||
ok := SafeDivide(10, 2)
|
||||
if ok.IsErr() {
|
||||
t.Error("SafeDivide(10, 2) should not error")
|
||||
}
|
||||
if ok.Unwrap() != 5.0 {
|
||||
t.Errorf("SafeDivide(10, 2) = %f, want 5.0", ok.Unwrap())
|
||||
}
|
||||
|
||||
err := SafeDivide(10, 0)
|
||||
if !err.IsErr() {
|
||||
t.Error("SafeDivide(10, 0) should error")
|
||||
}
|
||||
}
|
||||
GOEOF
|
||||
|
||||
# --- Makefile ---
|
||||
log_step "Generando Makefile..."
|
||||
cat > "$PROJECT_DIR/Makefile" << MKEOF
|
||||
.PHONY: build test clean python dev
|
||||
|
||||
MODULE_NAME := $MODULE_NAME
|
||||
BUILD_DIR := build
|
||||
SO_NAME := $SO_NAME
|
||||
|
||||
## build: Compila la shared library (.so) para Python
|
||||
build:
|
||||
@mkdir -p \$(BUILD_DIR)
|
||||
cd export && CGO_ENABLED=1 go build -buildmode=c-shared -o ../\$(BUILD_DIR)/\$(SO_NAME) .
|
||||
@echo "✓ Built \$(BUILD_DIR)/\$(SO_NAME)"
|
||||
|
||||
## test: Ejecuta tests de Go
|
||||
test:
|
||||
go test ./core/... ./shell/... -v
|
||||
|
||||
## python: Compila y ejecuta ejemplo Python
|
||||
python: build
|
||||
cd python && python3 example.py
|
||||
|
||||
## clean: Limpia artefactos
|
||||
clean:
|
||||
rm -rf \$(BUILD_DIR)
|
||||
find . -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true
|
||||
|
||||
## dev: Tests + build en un paso
|
||||
dev: test build
|
||||
@echo "✓ Ready — run 'make python' to test bindings"
|
||||
|
||||
## tidy: go mod tidy
|
||||
tidy:
|
||||
go mod tidy
|
||||
|
||||
## help: Muestra esta ayuda
|
||||
help:
|
||||
@grep -E '^## ' Makefile | sed 's/## //' | column -t -s ':'
|
||||
MKEOF
|
||||
|
||||
# --- .gitignore ---
|
||||
cat > "$PROJECT_DIR/.gitignore" << 'EOF'
|
||||
build/
|
||||
*.so
|
||||
*.h
|
||||
*.dylib
|
||||
*.dll
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.pytest_cache/
|
||||
EOF
|
||||
|
||||
# --- go mod tidy ---
|
||||
log_step "Ejecutando go mod tidy..."
|
||||
cd "$PROJECT_DIR"
|
||||
if [[ -f "go.work" ]]; then
|
||||
go mod tidy 2>/dev/null || log_warn "go mod tidy falló — revisa el go.work"
|
||||
else
|
||||
go mod tidy 2>/dev/null || log_warn "go mod tidy falló — DevFactory no está disponible"
|
||||
fi
|
||||
|
||||
# --- Resumen ---
|
||||
echo ""
|
||||
log_ok "Módulo '$MODULE_NAME' creado en $PROJECT_DIR"
|
||||
echo ""
|
||||
echo -e "${CYAN}Estructura:${NC}"
|
||||
echo " $MODULE_NAME/"
|
||||
echo " ├── core/ — Funciones puras (sin side effects)"
|
||||
echo " ├── shell/ — Operaciones I/O con Result[T]"
|
||||
echo " ├── export/ — Funciones exportadas via CGO"
|
||||
echo " ├── python/bindings — Wrapper ctypes auto-generado"
|
||||
echo " ├── Makefile — build, test, python, clean"
|
||||
echo " └── go.work — Enlace a DevFactory"
|
||||
echo ""
|
||||
echo -e "${CYAN}Comandos:${NC}"
|
||||
echo " make test — Ejecutar tests Go"
|
||||
echo " make build — Compilar shared library"
|
||||
echo " make python — Testear bindings Python"
|
||||
echo " make dev — Test + build en un paso"
|
||||
echo ""
|
||||
echo "STATUS: READY"
|
||||
Reference in New Issue
Block a user