En Windows el stdout de Python defaultea a cp1252 y crashea con
UnicodeEncodeError al imprimir literales no-ASCII (e.g. el ✓ del print
de éxito de pr-create.sh). El sintoma era que el PR se creaba OK pero
el script terminaba con exit 1 por el crash del print posterior.
Agrego export PYTHONIOENCODING=utf-8 después de set -euo pipefail en
los 10 scripts que invocan python (todos menos query.sh). Cubre tanto
las invocaciones inline como futuras sin tener que acordarse caso por
caso. Mantiene los literales unicode existentes (✓, →, ─, ⚠️).
224 lines
7.6 KiB
Bash
224 lines
7.6 KiB
Bash
#!/usr/bin/env bash
|
|
# gitea skill — crear un repo en gitea.nucleoriofrio.com.
|
|
#
|
|
# Soporta dos targets:
|
|
# - usuario autenticado (claudecode0): POST /api/v1/user/repos
|
|
# - org (NucleOS u otra): POST /api/v1/orgs/{org}/repos
|
|
#
|
|
# Regla dura: cualquier repo bajo el usuario `claudecode0` (case-insensitive
|
|
# match con $GITEA_BOT_USER) DEBE ser público. La justificación es que el
|
|
# usuario humano necesita poder ver lo que el bot crea — sino la cuenta
|
|
# claudecode0 sería una caja negra. Si pasás --private a un repo bajo el bot,
|
|
# el script aborta con exit 5.
|
|
#
|
|
# Para repos en orgs, --public o --private es REQUERIDO (forzar decisión
|
|
# explícita; sin default sorpresa).
|
|
#
|
|
# Uso:
|
|
# repo-create.sh <name> [--owner <user|org>] [--description "..."]
|
|
# [--public | --private] [--no-init] [--license <tpl>]
|
|
# [--gitignore <tpl>] [--default-branch <name>] [--template]
|
|
|
|
set -euo pipefail
|
|
|
|
# Forzar UTF-8 en stdout de Python (Windows defaultea a cp1252 y crashea con
|
|
# literales no-ASCII). Ver memoria feedback_api_utf8_encoding.md.
|
|
export PYTHONIOENCODING=utf-8
|
|
|
|
SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
QUERY="$SKILL_DIR/scripts/query.sh"
|
|
ENV_FILE="$SKILL_DIR/.env"
|
|
|
|
if [[ ! -f "$ENV_FILE" ]]; then
|
|
echo "ERROR: $ENV_FILE no existe. Corré setup.sh primero." >&2
|
|
exit 1
|
|
fi
|
|
|
|
set -a
|
|
# shellcheck disable=SC1090
|
|
source "$ENV_FILE"
|
|
set +a
|
|
|
|
: "${GITEA_BOT_USER:?GITEA_BOT_USER no definido en .env. Re-correr setup.sh.}"
|
|
|
|
name=""
|
|
owner=""
|
|
description=""
|
|
visibility="" # "public" | "private" | ""
|
|
auto_init=1
|
|
license=""
|
|
gitignore=""
|
|
default_branch="main"
|
|
is_template=0
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--owner) owner="$2"; shift 2 ;;
|
|
--description) description="$2"; shift 2 ;;
|
|
--public) visibility="public"; shift ;;
|
|
--private) visibility="private"; shift ;;
|
|
--no-init) auto_init=0; shift ;;
|
|
--license) license="$2"; shift 2 ;;
|
|
--gitignore) gitignore="$2"; shift 2 ;;
|
|
--default-branch) default_branch="$2"; shift 2 ;;
|
|
--template) is_template=1; shift ;;
|
|
-h|--help)
|
|
cat <<EOF
|
|
Uso: repo-create.sh <name> [opciones]
|
|
|
|
Argumentos:
|
|
<name> Nombre del repo (requerido)
|
|
|
|
Owner:
|
|
--owner <user|org> Default: el usuario del bot (\$GITEA_BOT_USER, "$GITEA_BOT_USER")
|
|
Pasale "NucleOS" para crear en el org.
|
|
|
|
Visibilidad:
|
|
--public | --private Para orgs, uno es REQUERIDO (sin default).
|
|
Para el usuario del bot, --private está
|
|
BLOQUEADO (regla dura: todo lo del bot debe ser
|
|
público para que el usuario humano pueda verlo).
|
|
|
|
Contenido:
|
|
--description "..." Descripción del repo
|
|
--no-init No crear README inicial (default: auto-init=true)
|
|
--license <template> ej. "MIT", "Apache-2.0", "GPL-3.0"
|
|
--gitignore <template> ej. "Node", "Python", "Go"
|
|
--default-branch <name> default: main
|
|
--template Marcar como template repo
|
|
|
|
Devuelve URL del repo creado (clone_url + html_url).
|
|
EOF
|
|
exit 0
|
|
;;
|
|
-*) echo "ERROR: flag desconocida: $1" >&2; exit 2 ;;
|
|
*)
|
|
if [[ -z "$name" ]]; then name="$1"
|
|
else echo "ERROR: argumento extra: $1" >&2; exit 2
|
|
fi
|
|
shift
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ -z "$name" ]]; then
|
|
echo "ERROR: pasá <name>." >&2
|
|
echo "Uso: repo-create.sh <name> [opciones] (--help para detalles)" >&2
|
|
exit 2
|
|
fi
|
|
|
|
# Default owner = bot user
|
|
if [[ -z "$owner" ]]; then
|
|
owner="$GITEA_BOT_USER"
|
|
fi
|
|
|
|
# ─── Detectar si owner es el bot (case-insensitive) ─────────────────────
|
|
shopt -s nocasematch
|
|
is_bot_owner=0
|
|
if [[ "$owner" == "$GITEA_BOT_USER" ]]; then
|
|
is_bot_owner=1
|
|
fi
|
|
shopt -u nocasematch
|
|
|
|
# ─── Guard: visibilidad ─────────────────────────────────────────────────
|
|
if [[ "$is_bot_owner" -eq 1 ]]; then
|
|
# Regla dura: bot user → siempre público
|
|
if [[ "$visibility" == "private" ]]; then
|
|
cat >&2 <<EOF
|
|
ERROR: --private bloqueado para repos bajo el usuario del bot ($GITEA_BOT_USER).
|
|
|
|
Regla dura de la skill: todo lo que el bot crea bajo su propio usuario debe
|
|
ser PÚBLICO para que el usuario humano pueda auditarlo. Sin esa regla, la
|
|
cuenta del bot sería una caja negra.
|
|
|
|
Opciones:
|
|
- Crearlo público (omitir --private; será público por default)
|
|
- Crearlo en una org en vez del usuario del bot:
|
|
repo-create.sh $name --owner NucleOS --private
|
|
EOF
|
|
exit 5
|
|
fi
|
|
# Default: público
|
|
visibility="public"
|
|
else
|
|
# Org u otro user → visibilidad explícita requerida (sin default sorpresa)
|
|
if [[ -z "$visibility" ]]; then
|
|
cat >&2 <<EOF
|
|
ERROR: para repos en una org (owner=$owner), --public o --private es REQUERIDO.
|
|
|
|
Sin default para evitar sorpresas. Pasá una flag explícita:
|
|
repo-create.sh $name --owner $owner --public
|
|
repo-create.sh $name --owner $owner --private
|
|
EOF
|
|
exit 6
|
|
fi
|
|
fi
|
|
|
|
# Convertir visibilidad → boolean private
|
|
case "$visibility" in
|
|
public) is_private="false" ;;
|
|
private) is_private="true" ;;
|
|
*) echo "ERROR interno: visibility=$visibility" >&2; exit 99 ;;
|
|
esac
|
|
|
|
# ─── Construir endpoint ─────────────────────────────────────────────────
|
|
if [[ "$is_bot_owner" -eq 1 ]]; then
|
|
endpoint="/user/repos"
|
|
else
|
|
endpoint="/orgs/${owner}/repos"
|
|
fi
|
|
|
|
# ─── Build body con Python (UTF-8 safe, mismo patrón que pr-create) ─────
|
|
TMP_DIR="$(mktemp -d)"
|
|
trap 'rm -rf "$TMP_DIR"' EXIT
|
|
BODY_JSON="$TMP_DIR/body.json"
|
|
|
|
PY_NAME="$name" PY_DESC="$description" PY_PRIVATE="$is_private" \
|
|
PY_AUTO_INIT="$auto_init" PY_LICENSE="$license" PY_GITIGNORE="$gitignore" \
|
|
PY_DEFAULT_BRANCH="$default_branch" PY_TEMPLATE="$is_template" \
|
|
python -c '
|
|
import json, os
|
|
data = {
|
|
"name": os.environ["PY_NAME"],
|
|
"description": os.environ.get("PY_DESC", ""),
|
|
"private": os.environ["PY_PRIVATE"] == "true",
|
|
"auto_init": os.environ["PY_AUTO_INIT"] == "1",
|
|
"default_branch": os.environ["PY_DEFAULT_BRANCH"],
|
|
"template": os.environ["PY_TEMPLATE"] == "1",
|
|
}
|
|
if os.environ.get("PY_LICENSE"):
|
|
data["license"] = os.environ["PY_LICENSE"]
|
|
if os.environ.get("PY_GITIGNORE"):
|
|
data["gitignores"] = os.environ["PY_GITIGNORE"]
|
|
print(json.dumps(data))
|
|
' > "$BODY_JSON"
|
|
|
|
# ─── POST ───────────────────────────────────────────────────────────────
|
|
echo "→ POST $endpoint" >&2
|
|
echo " name: $name" >&2
|
|
echo " owner: $owner $([ "$is_bot_owner" -eq 1 ] && echo "(bot user)" || echo "(org)")" >&2
|
|
echo " visibility: $visibility" >&2
|
|
echo " auto_init: $auto_init default_branch: $default_branch" >&2
|
|
[[ -n "$description" ]] && echo " description: $description" >&2
|
|
[[ -n "$license" ]] && echo " license: $license" >&2
|
|
[[ -n "$gitignore" ]] && echo " gitignore: $gitignore" >&2
|
|
|
|
resp="$("$QUERY" -X POST \
|
|
-H 'Content-Type: application/json; charset=utf-8' \
|
|
--data-binary @"$BODY_JSON" \
|
|
"$endpoint")"
|
|
|
|
echo "$resp" | PYTHONIOENCODING=utf-8 python -c "
|
|
import json, sys
|
|
d = json.load(sys.stdin)
|
|
if d.get('id') and d.get('full_name'):
|
|
visibility_label = 'private' if d.get('private') else 'public'
|
|
print(f'OK repo creado: {d[\"full_name\"]} ({visibility_label})')
|
|
print(f' html_url: {d.get(\"html_url\")}')
|
|
print(f' clone_url: {d.get(\"clone_url\")}')
|
|
print(f' ssh_url: {d.get(\"ssh_url\")}')
|
|
else:
|
|
print('ERROR:', d.get('message') or d, file=sys.stderr)
|
|
sys.exit(1)
|
|
"
|