c36aa18c67
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>
467 lines
13 KiB
Bash
Executable File
467 lines
13 KiB
Bash
Executable File
#!/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"
|