Agrega gitea_skill: skill para operar Gitea self-hosted (scripts API BOM-safe + push headless sin persistir token, SKILL/TOOLS/api-index). Actualiza CLAUDE.md, README.md, .env.example.

This commit is contained in:
urieljareth
2026-07-19 09:35:52 -06:00
parent f587a8baa2
commit 3b7209dcc1
11 changed files with 823 additions and 0 deletions
+81
View File
@@ -0,0 +1,81 @@
<#
.SYNOPSIS
Get or search a Gitea repository.
.DESCRIPTION
Three modes:
-Name <repo> -> GET /repos/{owner}/{name} (default owner = auth user)
-Search <term> -> GET /repos/search?q=<term>
-List -> GET /repos/search (all visible to the token)
.PARAMETER Owner
Owner (user or org). Defaults to the authenticated user.
.PARAMETER Name
Exact repository name to look up.
.PARAMETER Search
Free-text search term.
.PARAMETER List
Switch: list all visible repos.
.EXAMPLE
.\Get-GiteaRepo.ps1 -Name Proxmox-Coolify-Manager
.\Get-GiteaRepo.ps1 -Search manager
.\Get-GiteaRepo.ps1 -List
#>
param(
[string]$Owner,
[Parameter(ParameterSetName = "ByName")]
[string]$Name,
[Parameter(ParameterSetName = "BySearch")]
[string]$Search,
[Parameter(ParameterSetName = "ListAll")]
[switch]$List
)
$ErrorActionPreference = "Stop"
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$api = Join-Path $here "Invoke-GiteaApi.ps1"
function Resolve-GiteaOwner {
param([string]$OwnerArg)
if ($OwnerArg) { return $OwnerArg }
$me = & $api -Path "/user"
return $me.login
}
switch ($PSCmdlet.ParameterSetName) {
"ByName" {
if (-not $Name) { throw "Provide -Name." }
$o = Resolve-GiteaOwner -OwnerArg $Owner
try {
$r = & $api -Path "/repos/$o/$Name"
[PSCustomObject]@{
full_name = $r.full_name
private = $r.private
default_branch = $r.default_branch
clone_url = $r.clone_url
html_url = $r.html_url
ssh_url = $r.ssh_url
updated_at = $r.updated_at
} | Format-List
} catch {
if ($_.Exception.Message -match "404") {
Write-Host "Repository not found: $o/$Name" -ForegroundColor Yellow
return
}
throw
}
}
"BySearch" {
$term = [uri]::EscapeDataString($Search)
$r = & $api -Path "/repos/search?q=$term&limit=50"
$r.data | Select-Object full_name, private, default_branch, updated_at | Format-Table -AutoSize
}
"ListAll" {
$r = & $api -Path "/repos/search?limit=50"
$r.data | Select-Object full_name, private, default_branch, updated_at | Format-Table -AutoSize
}
}
+98
View File
@@ -0,0 +1,98 @@
<#
.SYNOPSIS
Low-level Gitea REST API wrapper (curl-based, BOM-safe).
.DESCRIPTION
Mirrors Invoke-GitHubApi.ps1's design: uses curl.exe so JSON bodies are
written via --data-binary @file (no PowerShell Set-Content BOM, which
Gitea rejects with "invalid character '\u00ef' looking for beginning of
value"). Reads GITEA_URL + GITEA_TOKEN from the environment.
.PARAMETER Method
HTTP verb. Default GET.
.PARAMETER Path
Path under /api/v1 (with or without leading slash). Query params allowed.
.PARAMETER BodyJson
JSON body for write verbs. Validated as JSON before sending.
.PARAMETER Raw
Return a pscustomobject with Status, Headers, Body instead of parsed JSON.
.EXAMPLE
.\Invoke-GiteaApi.ps1 -Path "/version"
.\Invoke-GiteaApi.ps1 -Path "/user"
.\Invoke-GiteaApi.ps1 -Method POST -Path "/user/repos" -BodyJson $json
#>
param(
[ValidateSet("GET", "POST", "PATCH", "PUT", "DELETE")]
[string]$Method = "GET",
[Parameter(Mandatory = $true)]
[string]$Path,
[string]$BodyJson,
[switch]$Raw
)
$ErrorActionPreference = "Stop"
if ([string]::IsNullOrWhiteSpace($env:GITEA_TOKEN)) {
throw "Set GITEA_TOKEN before calling the Gitea API. Store it in .env.local.ps1 (gitignored)."
}
if (-not (Get-Command curl.exe -ErrorAction SilentlyContinue)) {
throw "curl.exe not found in PATH."
}
$base = if ($env:GITEA_URL) { $env:GITEA_URL.TrimEnd("/") } else { "https://gitea-hjwh0svsoo9p5w5kj2j6b1bd.urieljareth.org" }
$base = "$base/api/v1"
$cleanPath = $Path.TrimStart("/")
$uri = "$base/$cleanPath"
# Gitea accepts "token <T>" (canonical) and "Bearer <T>" (since 1.16+).
$curlArgs = @(
"-sS", "-i",
"-X", $Method.ToUpper(),
"-H", "Authorization: token $env:GITEA_TOKEN",
"-H", "Accept: application/json",
"-H", "User-Agent: gitea-skill"
)
$tmpBody = $null
if ($PSBoundParameters.ContainsKey("BodyJson")) {
try { $null = $BodyJson | ConvertFrom-Json } catch { throw "BodyJson is not valid JSON: $($_.Exception.Message)" }
$tmpBody = [System.IO.Path]::GetTempFileName()
[System.IO.File]::WriteAllText($tmpBody, $BodyJson, (New-Object System.Text.UTF8Encoding($false)))
$curlArgs += @("--data-binary", "@$tmpBody", "-H", "Content-Type: application/json")
}
try {
$output = & curl.exe @curlArgs $uri
$exitCode = $LASTEXITCODE
if ($exitCode -ne 0) { throw "Gitea API request failed with exit code $exitCode." }
$rawStr = ($output | Out-String)
$parts = $rawStr -split "(?:`r`n`r`n|`n`n)", 2
$headersBlock = if ($parts.Count -ge 1) { $parts[0] } else { "" }
$bodyBlock = if ($parts.Count -ge 2) { $parts[1] } else { "{}" }
$statusLine = ($headersBlock -split "`n" | Select-Object -First 1).Trim()
if ($statusLine -notmatch " 20[04-9] ") {
$snippet = $bodyBlock.Trim()
if ($snippet.Length -gt 500) { $snippet = $snippet.Substring(0, 500) + "..." }
throw "Gitea API HTTP error: $statusLine`nURI: $uri`nBody: $snippet"
}
if ($Raw) {
[pscustomobject]@{ Status = $statusLine; Headers = $headersBlock; Body = $bodyBlock }
} else {
try { $bodyBlock | ConvertFrom-Json }
catch { $bodyBlock }
}
}
finally {
if ($tmpBody -and (Test-Path -LiteralPath $tmpBody)) {
Remove-Item -LiteralPath $tmpBody -Force -ErrorAction SilentlyContinue
}
}
+99
View File
@@ -0,0 +1,99 @@
<#
.SYNOPSIS
Create a Gitea repository (idempotent).
.DESCRIPTION
Mirrors deploy_skill/scripts/New-GitHubRepo.ps1. If the repo already exists,
prints its URL and returns the existing object instead of failing.
.PARAMETER Name
Repository name (required).
.PARAMETER Description
Short description.
.PARAMETER Private
Switch. If set, creates a private repo. Default public.
.PARAMETER Owner
Optional. User (default) or org to create under.
.PARAMETER NoAutoInit
Skip the Gitea-side README init (use when you will push an existing local
repo with its own history).
.EXAMPLE
.\New-GiteaRepo.ps1 -Name my-app -Description "demo" -Private
.\New-GiteaRepo.ps1 -Name my-app -NoAutoInit
#>
param(
[Parameter(Mandatory = $true)]
[ValidatePattern('^[A-Za-z0-9._-]+$')]
[string]$Name,
[string]$Description = "",
[switch]$Private,
[string]$Owner,
[switch]$NoAutoInit
)
$ErrorActionPreference = "Stop"
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$api = Join-Path $here "Invoke-GiteaApi.ps1"
$me = & $api -Path "/user"
$effectiveOwner = if ($Owner) { $Owner } else { $me.login }
Write-Host "Authenticated Gitea user: $($me.login)" -ForegroundColor Cyan
$existing = $null
try {
$existing = & $api -Path "/repos/$effectiveOwner/$Name"
} catch {
if ($_.Exception.Message -notmatch "404") { throw }
$existing = $null
}
if ($existing) {
Write-Host "Repository already exists: $($existing.html_url)" -ForegroundColor Yellow
[PSCustomObject]@{
full_name = $existing.full_name
html_url = $existing.html_url
clone_url = $existing.clone_url
ssh_url = $existing.ssh_url
private = $existing.private
default_branch = $existing.default_branch
} | Format-List
return $existing
}
$body = [ordered]@{
name = $Name
description = $Description
private = [bool]$Private
auto_init = (-not $NoAutoInit)
has_issues = $true
has_wiki = $false
default_branch = "main"
} | ConvertTo-Json -Compress
$endpoint = if ($Owner -and $Owner -ne $me.login) {
"/orgs/$Owner/repos"
} else {
"/user/repos"
}
Write-Host "Creating repository at: $endpoint" -ForegroundColor Cyan
$created = & $api -Method POST -Path $endpoint -BodyJson $body
[PSCustomObject]@{
full_name = $created.full_name
html_url = $created.html_url
clone_url = $created.clone_url
ssh_url = $created.ssh_url
private = $created.private
default_branch = $created.default_branch
} | Format-List
$created
+140
View File
@@ -0,0 +1,140 @@
<#
.SYNOPSIS
Wire a local git repo to Gitea and push the current branch headlessly.
.DESCRIPTION
1. Ensures the target Gitea repo exists (creates it if -CreateIfMissing).
2. Adds (or updates) a git remote pointing at Gitea. Default remote name
is 'gitea' so it sits alongside a GitHub 'origin' without conflict.
3. Pushes the current branch with a one-shot Authorization header so the
Gitea token is NOT persisted into .git/config or wincred.
The token is injected via `git -c http.extraHeader=...` for the single
push invocation only. credential.helper is blanked for that push so the
helper does not prompt and does not cache the header.
.PARAMETER AppPath
Path to the local git working copy. Defaults to current directory.
.PARAMETER Name
Repository name on Gitea. Defaults to the basename of AppPath.
.PARAMETER Owner
Owner (user or org). Defaults to the authenticated Gitea user.
.PARAMETER RemoteName
Remote name to add/update. Default 'gitea'.
.PARAMETER Branch
Branch to push. Defaults to the current HEAD of AppPath.
.PARAMETER CreateIfMissing
Create the Gitea repo if it does not exist. If omitted and the repo is
missing, the script aborts with a clear message.
.PARAMETER Private
Only used when -CreateIfMissing creates a new repo.
.PARAMETER Force
Skip the interactive confirmation before pushing.
.EXAMPLE
.\Sync-GiteaRemote.ps1 -AppPath .\my-app -CreateIfMissing
.\Sync-GiteaRemote.ps1 -Name Proxmox-Coolify-Manager -RemoteName origin -Force
#>
param(
[string]$AppPath = (Get-Location).Path,
[string]$Name,
[string]$Owner,
[string]$RemoteName = "gitea",
[string]$Branch,
[switch]$CreateIfMissing,
[switch]$Private,
[switch]$Force
)
$ErrorActionPreference = "Stop"
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$api = Join-Path $here "Invoke-GiteaApi.ps1"
$newRepo = Join-Path $here "New-GiteaRepo.ps1"
if ([string]::IsNullOrWhiteSpace($env:GITEA_TOKEN)) {
throw "Set GITEA_TOKEN (and GITEA_URL) before running this. Load .\.env.local.ps1."
}
$repoPath = (Resolve-Path -LiteralPath $AppPath).ProviderPath
if (-not (Test-Path -LiteralPath (Join-Path $repoPath ".git"))) {
throw "Not a git repository: $repoPath"
}
$me = & $api -Path "/user"
$effectiveOwner = if ($Owner) { $Owner } else { $me.login }
$effectiveName = if ($Name) { $Name } else { (Split-Path -Leaf $repoPath) }
if (-not $Branch) {
$Branch = (& git -C $repoPath rev-parse --abbrev-ref HEAD).Trim()
}
if ([string]::IsNullOrWhiteSpace($Branch) -or $Branch -eq "HEAD") {
throw "Could not determine current branch in $repoPath. Pass -Branch explicitly."
}
Write-Host "[*] repo=$effectiveOwner/$effectiveName branch=$Branch remote=$RemoteName" -ForegroundColor Cyan
$repo = $null
try {
$repo = & $api -Path "/repos/$effectiveOwner/$effectiveName"
} catch {
if ($_.Exception.Message -notmatch "404") { throw }
$repo = $null
}
if (-not $repo) {
if (-not $CreateIfMissing) {
throw "Gitea repo $effectiveOwner/$effectiveName does not exist. Re-run with -CreateIfMissing, or create it first with New-GiteaRepo.ps1."
}
Write-Host "[*] Creating missing repo $effectiveOwner/$effectiveName ..." -ForegroundColor Cyan
& $newRepo -Name $effectiveName -Owner $effectiveOwner -Private:$Private -NoAutoInit | Out-Null
$repo = & $api -Path "/repos/$effectiveOwner/$effectiveName"
}
$giteaHost = if ($env:GITEA_URL) { $env:GITEA_URL.TrimEnd("/") } else { "https://gitea-hjwh0svsoo9p5w5kj2j6b1bd.urieljareth.org" }
$remoteUrl = "$giteaHost/$effectiveOwner/$effectiveName.git"
$existingRemote = $null
try {
$existingRemote = & git -C $repoPath remote get-url $RemoteName 2>$null
} catch { $existingRemote = $null }
if ($existingRemote) {
if ($existingRemote.Trim() -ne $remoteUrl) {
Write-Host "[*] Updating remote '$RemoteName' URL -> $remoteUrl" -ForegroundColor Yellow
& git -C $repoPath remote set-url $RemoteName $remoteUrl
} else {
Write-Host "[*] Remote '$RemoteName' already -> $remoteUrl" -ForegroundColor DarkGray
}
} else {
Write-Host "[*] Adding remote '$RemoteName' -> $remoteUrl" -ForegroundColor Cyan
& git -C $repoPath remote add $RemoteName $remoteUrl
}
if (-not $Force) {
$answer = Read-Host "Push '$Branch' to $RemoteName ($effectiveOwner/$effectiveName)? [y/N]"
if ($answer -notmatch '^[yY]') {
Write-Host "Aborted (no push)." -ForegroundColor Yellow
return
}
}
Write-Host "[*] Pushing $Branch -> $RemoteName ..." -ForegroundColor Cyan
$env:GIT_TERMINAL_PROMPT = "0"
& git -C $repoPath `
-c "http.extraHeader=Authorization: token $env:GITEA_TOKEN" `
-c "credential.helper=" `
push -u $RemoteName $Branch
$pushExit = $LASTEXITCODE
if ($pushExit -ne 0) {
throw "git push failed (exit $pushExit). Check the token scopes (needs 'write:repository')."
}
Write-Host ""
Write-Host "[OK] Pushed to $($repo.html_url)" -ForegroundColor Green
Write-Host " remote '$RemoteName' -> $remoteUrl"
@@ -0,0 +1,68 @@
<#
.SYNOPSIS
Gitea smoke test: config + API auth + token scopes.
.DESCRIPTION
Calls /version, /user (auth), /settings/api, and lists the authenticated
user's repos. Exits 1 on any FAIL so it composes into CI / agent startup.
.EXAMPLE
.\Test-GiteaConnection.ps1
#>
$ErrorActionPreference = "Stop"
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$api = Join-Path $here "Invoke-GiteaApi.ps1"
if ([string]::IsNullOrWhiteSpace($env:GITEA_TOKEN)) {
Write-Host "[FAIL] GITEA_TOKEN not set. Load .\.env.local.ps1 first." -ForegroundColor Red
exit 1
}
if ([string]::IsNullOrWhiteSpace($env:GITEA_URL)) {
Write-Host "[WARN] GITEA_URL not set; using default https://gitea-hjwh0svsoo9p5w5kj2j6b1bd.urieljareth.org" -ForegroundColor Yellow
}
$failures = 0
# 1) Version (anonymous, but proves reachability)
try {
$v = & $api -Path "/version"
Write-Host "[PASS] /version -> Gitea $($v.version)" -ForegroundColor Green
} catch {
Write-Host "[FAIL] /version: $($_.Exception.Message)" -ForegroundColor Red
$failures++
}
# 2) Authenticated user (proves token validity)
try {
$u = & $api -Path "/user"
Write-Host "[PASS] /user -> login='$($u.login)' is_admin=$($u.is_admin)" -ForegroundColor Green
} catch {
Write-Host "[FAIL] /user (token invalid or expired): $($_.Exception.Message)" -ForegroundColor Red
$failures++
}
# 3) Token scopes (Gitea 1.20+: /users/me/tokens only lists tokens created via API; for app tokens, /user suffices)
try {
$settings = & $api -Path "/settings/api"
Write-Host "[PASS] /settings/api -> max_response_items=$($settings.max_response_items)" -ForegroundColor Green
} catch {
Write-Host "[WARN] /settings/api unavailable on this version: $($_.Exception.Message)" -ForegroundColor Yellow
}
# 4) List repos (proves read scope on /repos/search)
try {
$repos = & $api -Path "/repos/search?limit=50"
$count = @($repos.data).Count
Write-Host "[PASS] /repos/search -> $count repos visible" -ForegroundColor Green
} catch {
Write-Host "[FAIL] /repos/search: $($_.Exception.Message)" -ForegroundColor Red
$failures++
}
Write-Host ""
if ($failures -gt 0) {
Write-Host "[RESULT] $failures FAIL(s). Gitea skill NOT ready." -ForegroundColor Red
exit 1
}
Write-Host "[RESULT] OK. Gitea skill ready." -ForegroundColor Green