feat: add bash infra functions — Gitea, Android SDK, Mantine, Capacitor

Nuevas funciones bash: gestión Gitea (create_repo, list_repos, add_collaborator,
push_directory), install_android_sdk, install_mantine, frontend_doctor.
Pipelines: capacitor_build_apk y gitea_init_app.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-06 23:47:10 +02:00
parent 817ec9fc36
commit 01042bc23c
18 changed files with 1466 additions and 0 deletions
@@ -0,0 +1,76 @@
---
name: capacitor_build_apk
kind: pipeline
lang: bash
domain: pipelines
version: "1.0.0"
purity: impure
signature: "capacitor_build_apk(web_app_dir: string, [app_id: string], [app_name: string]) -> void"
description: "Pipeline que convierte una web app en un APK de Android usando Capacitor. Valida el entorno (ANDROID_HOME, Java 17+), construye el bundle web si no existe dist/, inicializa Capacitor si no está configurado, añade la plataforma Android, sincroniza y compila el APK con Gradle. El APK final queda en el directorio raíz de la web app."
tags: [android, apk, capacitor, mobile, build, pipeline, bash]
uses_functions:
- install_android_sdk_bash_infra
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: web_app_dir
desc: "directorio raíz de la web app; debe contener package.json; si no existe dist/ se ejecuta pnpm build automáticamente"
- name: app_id
desc: "identificador de la app Android en formato reverse-DNS (default: com.fnregistry.app)"
- name: app_name
desc: "nombre visible de la app Android; si se omite, se lee del campo name de package.json"
output: "APK de debug en <web_app_dir>/<app_name>.apk; imprime ruta y tamaño en MB al finalizar"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/pipelines/capacitor_build_apk.sh"
---
## Ejemplo
```bash
# Build con defaults (app-id y app-name desde package.json)
./bash/functions/pipelines/capacitor_build_apk.sh ~/projects/my-web-app
# Build especificando app-id y app-name
./bash/functions/pipelines/capacitor_build_apk.sh ~/projects/my-web-app \
--app-id com.miempresa.miapp \
--app-name "Mi Aplicación"
```
## Flujo
1. **Validación** — verifica que `web_app_dir` existe, tiene `package.json`, que `ANDROID_HOME` está seteado (o sourcea `$HOME/android-sdk/env.sh`) y que Java 17+ está disponible.
2. **Build web** — si no existe `dist/`, ejecuta `pnpm build` en el directorio de la app.
3. **Init Capacitor** — si no existe `capacitor.config.ts`, instala `@capacitor/core`, `@capacitor/cli` y `@capacitor/android` via npm y genera el archivo de configuración con el `appId`, `appName` y `webDir: dist`.
4. **Add Android** — si no existe el directorio `android/`, ejecuta `npx cap add android`.
5. **Sync** — ejecuta `npx cap sync android` para copiar los assets web al proyecto Android.
6. **Build APK** — ejecuta `./gradlew assembleDebug` desde `android/`; si falla sale con exit 1.
7. **Copia APK** — copia `android/app/build/outputs/apk/debug/app-debug.apk` a `<web_app_dir>/<app_name>.apk`.
8. **Resultado** — imprime la ruta del APK y su tamaño en MB.
## Requisitos
- **Node.js** y **pnpm** disponibles en PATH
- **Java 17+** disponible en PATH
- **Android SDK** instalado: `ANDROID_HOME` seteado, o bien `$HOME/android-sdk/env.sh` existente (generado por `install_android_sdk`)
- **Gradle wrapper** presente en el directorio `android/` (generado por `cap add android`)
## Notas
El pipeline usa `set -euo pipefail` — cualquier fallo detiene la ejecución inmediatamente.
El APK generado es un **debug build**, apto para desarrollo y pruebas. Para publicar en Play Store se necesita un release build firmado (`assembleRelease` con un keystore).
`install_android_sdk_bash_infra` se referencia como dependencia previa: el usuario debe haberlo ejecutado (o haber instalado el SDK manualmente) antes de invocar este pipeline.
La detección del `app_name` desde `package.json` usa `node -e` inline, lo que requiere que Node.js esté disponible. Si el campo `name` no existe en el JSON, se usa el valor por defecto `app`.
Para instalar el APK en un dispositivo Android conectado por USB (con depuración USB activada):
```bash
adb install <web_app_dir>/<app_name>.apk
```
@@ -0,0 +1,208 @@
#!/usr/bin/env bash
# capacitor_build_apk
# -------------------
# Pipeline que convierte una web app buildeada en un APK de Android usando Capacitor.
# Asume que el Android SDK está instalado (via install_android_sdk o manualmente).
#
# USO:
# ./capacitor_build_apk.sh <web_app_dir> [--app-id com.example.app] [--app-name "My App"]
#
# ARGUMENTOS:
# web_app_dir Directorio de la web app (debe contener package.json)
# --app-id ID de la app Android (default: com.fnregistry.app)
# --app-name Nombre visible de la app (default: name de package.json)
#
# REQUISITOS:
# - Node.js + pnpm instalados en PATH
# - Java 17+ instalado en PATH
# - Android SDK: ANDROID_HOME seteado o $HOME/android-sdk/env.sh disponible
set -euo pipefail
# ---------------------------------------------------------------------------
# Parseo de argumentos
# ---------------------------------------------------------------------------
WEB_APP_DIR=""
APP_ID="com.fnregistry.app"
APP_NAME=""
while [[ $# -gt 0 ]]; do
case "$1" in
--app-id)
APP_ID="$2"
shift 2
;;
--app-name)
APP_NAME="$2"
shift 2
;;
-*)
echo "[capacitor_build_apk] ERROR: argumento desconocido: $1" >&2
echo "USO: $0 <web_app_dir> [--app-id com.example.app] [--app-name \"My App\"]" >&2
exit 1
;;
*)
WEB_APP_DIR="$1"
shift
;;
esac
done
if [[ -z "$WEB_APP_DIR" ]]; then
echo "[capacitor_build_apk] ERROR: web_app_dir es obligatorio." >&2
echo "USO: $0 <web_app_dir> [--app-id com.example.app] [--app-name \"My App\"]" >&2
exit 1
fi
# ---------------------------------------------------------------------------
# 1. Validación
# ---------------------------------------------------------------------------
echo "[capacitor_build_apk] Validando entorno..."
# Verificar que web_app_dir existe y tiene package.json
if [[ ! -d "$WEB_APP_DIR" ]]; then
echo "[capacitor_build_apk] ERROR: directorio no existe: $WEB_APP_DIR" >&2
exit 1
fi
if [[ ! -f "$WEB_APP_DIR/package.json" ]]; then
echo "[capacitor_build_apk] ERROR: no se encontró package.json en $WEB_APP_DIR" >&2
exit 1
fi
# Resolver app name desde package.json si no se pasó
if [[ -z "$APP_NAME" ]]; then
APP_NAME=$(node -e "const p = require('$WEB_APP_DIR/package.json'); process.stdout.write(p.name || 'app');" 2>/dev/null || echo "app")
echo "[capacitor_build_apk] App name detectado desde package.json: $APP_NAME"
fi
# Verificar ANDROID_HOME o sourcea env.sh
if [[ -z "${ANDROID_HOME:-}" ]]; then
ANDROID_ENV="$HOME/android-sdk/env.sh"
if [[ -f "$ANDROID_ENV" ]]; then
echo "[capacitor_build_apk] ANDROID_HOME no seteado, sourceando $ANDROID_ENV ..."
# shellcheck source=/dev/null
source "$ANDROID_ENV"
else
echo "[capacitor_build_apk] ERROR: ANDROID_HOME no está seteado y no se encontró $ANDROID_ENV" >&2
echo " Instala el SDK con install_android_sdk o setea ANDROID_HOME manualmente." >&2
exit 1
fi
fi
echo "[capacitor_build_apk] ANDROID_HOME: $ANDROID_HOME"
# Verificar Java 17+
if ! command -v java &>/dev/null; then
echo "[capacitor_build_apk] ERROR: java no está en PATH." >&2
exit 1
fi
JAVA_VERSION=$(java -version 2>&1 | head -1 | grep -oP '(?<=version ")([0-9]+)' | head -1 || echo "0")
if [[ "$JAVA_VERSION" -lt 17 ]]; then
echo "[capacitor_build_apk] ERROR: se requiere Java 17+. Versión detectada: $JAVA_VERSION" >&2
exit 1
fi
echo "[capacitor_build_apk] Java $JAVA_VERSION detectado."
# ---------------------------------------------------------------------------
# 2. Build web (si no existe dist/)
# ---------------------------------------------------------------------------
if [[ ! -d "$WEB_APP_DIR/dist" ]]; then
echo "[capacitor_build_apk] No se encontró dist/, ejecutando pnpm build..."
(cd "$WEB_APP_DIR" && pnpm build)
echo "[capacitor_build_apk] Build web completado."
else
echo "[capacitor_build_apk] dist/ ya existe, omitiendo build web."
fi
# ---------------------------------------------------------------------------
# 3. Init Capacitor (si no existe capacitor.config.ts)
# ---------------------------------------------------------------------------
if [[ ! -f "$WEB_APP_DIR/capacitor.config.ts" ]]; then
echo "[capacitor_build_apk] Instalando dependencias de Capacitor..."
(cd "$WEB_APP_DIR" && npm install @capacitor/core @capacitor/cli @capacitor/android)
echo "[capacitor_build_apk] Generando capacitor.config.ts..."
cat > "$WEB_APP_DIR/capacitor.config.ts" <<CAPCONFIG
import type { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: '${APP_ID}',
appName: '${APP_NAME}',
webDir: 'dist',
server: { androidScheme: 'https' }
};
export default config;
CAPCONFIG
echo "[capacitor_build_apk] capacitor.config.ts generado."
else
echo "[capacitor_build_apk] capacitor.config.ts ya existe, omitiendo init."
fi
# ---------------------------------------------------------------------------
# 4. Add Android (si no existe el directorio android/)
# ---------------------------------------------------------------------------
if [[ ! -d "$WEB_APP_DIR/android" ]]; then
echo "[capacitor_build_apk] Añadiendo plataforma Android..."
(cd "$WEB_APP_DIR" && npx cap add android)
echo "[capacitor_build_apk] Plataforma Android añadida."
else
echo "[capacitor_build_apk] Directorio android/ ya existe, omitiendo cap add."
fi
# ---------------------------------------------------------------------------
# 5. Sync
# ---------------------------------------------------------------------------
echo "[capacitor_build_apk] Sincronizando assets web con Android..."
(cd "$WEB_APP_DIR" && npx cap sync android)
echo "[capacitor_build_apk] Sync completado."
# ---------------------------------------------------------------------------
# 6. Build APK
# ---------------------------------------------------------------------------
echo "[capacitor_build_apk] Compilando APK con Gradle..."
if ! (cd "$WEB_APP_DIR/android" && ./gradlew assembleDebug); then
echo "[capacitor_build_apk] ERROR: Gradle falló. Revisa los logs anteriores." >&2
exit 1
fi
APK_SOURCE="$WEB_APP_DIR/android/app/build/outputs/apk/debug/app-debug.apk"
if [[ ! -f "$APK_SOURCE" ]]; then
echo "[capacitor_build_apk] ERROR: Gradle terminó sin error pero no se encontró el APK en $APK_SOURCE" >&2
exit 1
fi
# ---------------------------------------------------------------------------
# 7. Copia APK al directorio raíz
# ---------------------------------------------------------------------------
APK_DEST="$WEB_APP_DIR/${APP_NAME}.apk"
cp "$APK_SOURCE" "$APK_DEST"
# ---------------------------------------------------------------------------
# 8. Resultado
# ---------------------------------------------------------------------------
APK_SIZE_BYTES=$(stat -c%s "$APK_DEST" 2>/dev/null || stat -f%z "$APK_DEST" 2>/dev/null || echo "0")
APK_SIZE_MB=$(awk "BEGIN {printf \"%.1f\", $APK_SIZE_BYTES/1048576}")
echo ""
echo "---------------------------------------------------------------------"
echo "APK generado: $APK_DEST"
echo "Tamaño: ${APK_SIZE_MB} MB"
echo ""
echo "Para instalar en un dispositivo conectado por USB:"
echo " adb install '$APK_DEST'"
echo "---------------------------------------------------------------------"
@@ -0,0 +1,67 @@
---
name: gitea_init_app
kind: pipeline
lang: bash
domain: pipelines
version: "1.0.0"
purity: impure
signature: "gitea_init_app(directory: string, owner: string, name: string, private: string) -> string"
description: "Pipeline que crea un repositorio en Gitea, sube el directorio local y añade a egutierrez como colaborador admin. Compone gitea_create_repo → gitea_push_directory → gitea_add_collaborator."
tags: [gitea, git, pipeline, repo, create, push, launcher, infra]
uses_functions:
- gitea_create_repo_bash_infra
- gitea_push_directory_bash_infra
- gitea_add_collaborator_bash_infra
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: directory
desc: "ruta al directorio local a subir como repositorio"
- name: owner
desc: "usuario u organización en Gitea que será propietaria del repo"
- name: name
desc: "nombre del repositorio (opcional: se infiere del basename del directorio)"
- name: private
desc: "si el repo debe ser privado, 'true' o 'false' (default: false)"
output: "URL del repositorio creado en Gitea"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/pipelines/gitea_init_app.sh"
---
## Ejemplo
```bash
export GITEA_URL="$(pass agentes/gitea-url)"
export GITEA_TOKEN="$(pass agentes/dataforge-token)"
# Crear repo con nombre inferido del directorio
bash bash/functions/pipelines/gitea_init_app.sh /home/lucas/myapp myorg
# Nombre explícito y repo privado
bash bash/functions/pipelines/gitea_init_app.sh /home/lucas/myapp myorg my-custom-name true
# Con flags
bash bash/functions/pipelines/gitea_init_app.sh \
--directory /home/lucas/myapp \
--owner myorg \
--name my-app \
--private true
```
## Pasos del pipeline
1. `gitea_create_repo owner name private` — crea el repo (idempotente si ya existe)
2. `gitea_push_directory directory owner repo` — inicializa git y hace push del directorio
3. `gitea_add_collaborator owner repo egutierrez admin` — añade colaborador con permisos admin
## Notas
- Requiere `GITEA_URL` y `GITEA_TOKEN` seteadas.
- Si el repo ya existe (409), el pipeline continúa con el push y añade el colaborador.
- El colaborador `egutierrez` es fijo en el pipeline — para variarlo usar las funciones individuales.
- La URL del repo se imprime a stdout al finalizar.
@@ -0,0 +1,79 @@
#!/usr/bin/env bash
# Pipeline: gitea_init_app — Crea repo en Gitea, sube directorio y añade colaborador egutierrez
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../infra/gitea_create_repo.sh"
source "$SCRIPT_DIR/../infra/gitea_push_directory.sh"
source "$SCRIPT_DIR/../infra/gitea_add_collaborator.sh"
main() {
local directory=""
local owner=""
local name=""
local private="false"
# Parsear argumentos
while [[ $# -gt 0 ]]; do
case "$1" in
--directory) directory="$2"; shift 2 ;;
--owner) owner="$2"; shift 2 ;;
--name) name="$2"; shift 2 ;;
--private) private="$2"; shift 2 ;;
*)
# Argumentos posicionales: directory owner [name] [private]
if [[ -z "$directory" ]]; then
directory="$1"
elif [[ -z "$owner" ]]; then
owner="$1"
elif [[ -z "$name" ]]; then
name="$1"
else
private="$1"
fi
shift
;;
esac
done
if [[ -z "$directory" || -z "$owner" ]]; then
echo "gitea_init_app: uso: gitea_init_app <directory> <owner> [name] [private]" >&2
echo "gitea_init_app: o con flags: --directory <dir> --owner <owner> [--name <name>] [--private true]" >&2
return 1
fi
# Inferir nombre del repo desde basename del directorio si no se especificó
if [[ -z "$name" ]]; then
name=$(basename "$directory")
echo "gitea_init_app: nombre inferido del directorio: '$name'" >&2
fi
if [[ -z "${GITEA_URL:-}" ]]; then
echo "gitea_init_app: GITEA_URL no está seteada" >&2
return 1
fi
if [[ -z "${GITEA_TOKEN:-}" ]]; then
echo "gitea_init_app: GITEA_TOKEN no está seteado" >&2
return 1
fi
echo "gitea_init_app: iniciando pipeline para '$owner/$name'..." >&2
echo "gitea_init_app: directorio fuente: '$directory'" >&2
# Paso 1: Crear repo
echo "gitea_init_app: [1/3] creando repositorio..." >&2
gitea_create_repo "$owner" "$name" "$private" "" > /dev/null
# Paso 2: Subir directorio
echo "gitea_init_app: [2/3] subiendo directorio al repositorio..." >&2
gitea_push_directory "$directory" "$owner" "$name"
# Paso 3: Añadir colaborador egutierrez con permisos admin
echo "gitea_init_app: [3/3] añadiendo colaborador egutierrez..." >&2
gitea_add_collaborator "$owner" "$name" "egutierrez" "admin"
echo "gitea_init_app: pipeline completado — ${GITEA_URL}/${owner}/${name}" >&2
echo "${GITEA_URL}/${owner}/${name}"
}
main "$@"