Добавить bootstrap для новых ПК и Linux-скрипты.

SSH-ключи и Gitea API до clone Rules; init-cursor копирует из локального $HOME/CursorRules.
This commit is contained in:
2026-06-18 10:01:11 +10:00
parent cd7513d96c
commit bdf405e2c1
13 changed files with 1147 additions and 13 deletions
+136
View File
@@ -0,0 +1,136 @@
#Requires -Version 5.1
<#
.SYNOPSIS
Регистрирует публичные SSH-ключи в Gitea через API (kalinamall, papatramp).
#>
[CmdletBinding()]
param(
[string]$ConfigDir = "$HOME\CursorRules\local",
[string]$HostsConfig = '',
[switch]$WhatIf
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
function Write-Step([string]$Message) { Write-Host "[*] $Message" -ForegroundColor Cyan }
function Write-Ok([string]$Message) { Write-Host "[+] $Message" -ForegroundColor Green }
function Write-Warn([string]$Message) { Write-Host "[!] $Message" -ForegroundColor Yellow }
function Read-KeyValueFile {
param([string]$Path)
$data = @{}
Get-Content -LiteralPath $Path | ForEach-Object {
if ($_ -match '^([^#=]+)=(.*)$') {
$data[$Matches[1].Trim()] = $Matches[2].Trim()
}
}
return $data
}
function Read-GitSshHostsConfig {
param([string]$Path)
$profiles = @{}
$currentProfile = $null
Get-Content -LiteralPath $Path | ForEach-Object {
$line = $_.Trim()
if (-not $line -or $line.StartsWith('#')) { return }
if ($line -match '^\[profile:(.+)\]$') {
$currentProfile = $Matches[1]
$profiles[$currentProfile] = @{}
return
}
if ($line -match '^\[host:') { $currentProfile = $null; return }
if ($line -match '^([^=]+)=(.*)$' -and $currentProfile) {
$profiles[$currentProfile][$Matches[1].Trim()] = $Matches[2].Trim()
}
}
return $profiles
}
function Get-GiteaAuthHeader {
param([hashtable]$Cfg)
$pair = '{0}:{1}' -f $Cfg.GiteaApiUser, $Cfg.GiteaApiPassword
$auth = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes($pair))
return @{ Authorization = $auth; 'Content-Type' = 'application/json' }
}
function Register-GiteaKey {
param(
[hashtable]$Cfg,
[string]$PublicKeyPath,
[string]$Title
)
if (-not $Cfg.GiteaApiPassword) {
Write-Warn "$Title : GiteaApiPassword empty - add key manually in Gitea UI"
Write-Host (Get-Content -LiteralPath $PublicKeyPath -Raw).Trim()
return
}
$base = $Cfg.GiteaApiUrl.TrimEnd('/')
$headers = Get-GiteaAuthHeader -Cfg $Cfg
$pub = (Get-Content -LiteralPath $PublicKeyPath -Raw).Trim()
$existing = Invoke-RestMethod -Method Get -Uri "$base/api/v1/user/keys" -Headers $headers -TimeoutSec 15
foreach ($item in @($existing)) {
if ($item.key.Trim() -eq $pub) {
Write-Ok "$Title : key already registered ($($item.title))"
return
}
}
$body = (@{ title = $Title; key = $pub } | ConvertTo-Json)
if ($WhatIf) {
Write-Step "WhatIf: POST $base/api/v1/user/keys ($Title)"
return
}
$created = Invoke-RestMethod -Method Post -Uri "$base/api/v1/user/keys" -Headers $headers -Body $body -TimeoutSec 30
Write-Ok "$Title : registered ($($created.title))"
}
if (-not $HostsConfig) {
$HostsConfig = Join-Path $ConfigDir 'git-ssh-hosts.conf'
}
if (-not (Test-Path -LiteralPath $HostsConfig)) {
throw "Config not found: $HostsConfig"
}
$profiles = Read-GitSshHostsConfig -Path $HostsConfig
$sshDir = Join-Path $HOME '.ssh'
$machine = $env:COMPUTERNAME
if (-not $machine) { $machine = [Environment]::MachineName }
$registered = @{}
foreach ($profileName in $profiles.Keys) {
$profile = $profiles[$profileName]
$giteaConfigName = $profile.GiteaConfig
if (-not $giteaConfigName) { continue }
$keyName = $profile.KeyName
if ($registered.ContainsKey($keyName)) { continue }
$registered[$keyName] = $true
$giteaPath = Join-Path $ConfigDir $giteaConfigName
if (-not (Test-Path -LiteralPath $giteaPath)) {
Write-Warn "Gitea config not found: $giteaPath"
continue
}
$cfg = Read-KeyValueFile -Path $giteaPath
$pubPath = Join-Path $sshDir "$keyName.pub"
if (-not (Test-Path -LiteralPath $pubPath)) {
Write-Warn "Public key not found: $pubPath (run Setup-GitSsh first)"
continue
}
$title = "$machine-$keyName"
Write-Step "Registering $keyName on $($cfg.GiteaHost)"
Register-GiteaKey -Cfg $cfg -PublicKeyPath $pubPath -Title $title
}
Write-Ok 'Gitea SSH key registration done'
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/env bash
# Стандартные git remote: home, kalinamall, github.
set -euo pipefail
OWNER="${OWNER:-PapaTramp}"
GITHUB_ORG="${GITHUB_ORG:-PTah}"
REPO="${REPO:-}"
SKIP_KALINAMALL="${SKIP_KALINAMALL:-0}"
WHAT_IF="${WHAT_IF:-0}"
log_step() { printf '[*] %s\n' "$1"; }
log_ok() { printf '[+] %s\n' "$1"; }
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo 'Run inside a git repository or set REPO.' >&2
exit 1
fi
repo_name="${REPO:-$(basename "$(git rev-parse --show-toplevel)")}"
home_url="ssh://git@git.papatramp.lan:2222/${OWNER}/${repo_name}.git"
kalinamall_url="ssh://git@git.kalinamall.lan:2222/${OWNER}/${repo_name}.git"
github_url="git@github.com:${GITHUB_ORG}/${repo_name}.git"
set_remote() {
local name="$1"
local url="$2"
local existing current old
if [[ "$WHAT_IF" == '1' ]]; then
log_step "WhatIf: remote $name -> $url"
return 0
fi
if git remote | grep -Fxq "$name"; then
current="$(git remote get-url "$name" | tr -d '\r\n')"
if [[ "$current" == "$url" ]]; then
log_ok "remote $name OK"
return 0
fi
git remote set-url "$name" "$url"
log_ok "remote $name updated -> $url"
return 0
fi
case "$name" in
home) legacy=(gitea papatramp origin-home) ;;
github) legacy=(origin) ;;
kalinamall) legacy=(gitea-kalinamall kalina) ;;
*) legacy=() ;;
esac
for old in "${legacy[@]}"; do
if git remote | grep -Fxq "$old"; then
git remote rename "$old" "$name"
git remote set-url "$name" "$url"
log_ok "remote $old renamed to $name -> $url"
return 0
fi
done
git remote add "$name" "$url"
log_ok "remote $name added -> $url"
}
log_step "Repo: $repo_name"
set_remote home "$home_url"
set_remote github "$github_url"
if [[ "$SKIP_KALINAMALL" != '1' ]]; then
set_remote kalinamall "$kalinamall_url"
fi
printf '\n'
git remote -v
+196
View File
@@ -0,0 +1,196 @@
#Requires -Version 5.1
<#
.SYNOPSIS
Генерирует SSH-ключи и ~/.ssh/config для Git-хостов из local/git-ssh-hosts.conf.
#>
[CmdletBinding()]
param(
[string]$ConfigDir = "$HOME\CursorRules\local",
[string]$HostsConfig = '',
[switch]$WhatIf
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
function Write-Step([string]$Message) { Write-Host "[*] $Message" -ForegroundColor Cyan }
function Write-Ok([string]$Message) { Write-Host "[+] $Message" -ForegroundColor Green }
function Write-Warn([string]$Message) { Write-Host "[!] $Message" -ForegroundColor Yellow }
function Read-KeyValueFile {
param([string]$Path)
$data = @{}
if (-not (Test-Path -LiteralPath $Path)) { return $data }
Get-Content -LiteralPath $Path | ForEach-Object {
if ($_ -match '^([^#=]+)=(.*)$') {
$data[$Matches[1].Trim()] = $Matches[2].Trim()
}
}
return $data
}
function Read-GitSshHostsConfig {
param([string]$Path)
$profiles = @{}
$hosts = @{}
$currentProfile = $null
$currentHost = $null
Get-Content -LiteralPath $Path | ForEach-Object {
$line = $_.Trim()
if (-not $line -or $line.StartsWith('#')) { return }
if ($line -match '^\[profile:(.+)\]$') {
$currentProfile = $Matches[1]
$currentHost = $null
$profiles[$currentProfile] = @{}
return
}
if ($line -match '^\[host:(.+)\]$') {
$currentHost = $Matches[1]
$currentProfile = $null
$hosts[$currentHost] = @{ Name = $currentHost }
return
}
if ($line -match '^([^=]+)=(.*)$') {
$key = $Matches[1].Trim()
$value = $Matches[2].Trim()
if ($currentHost) { $hosts[$currentHost][$key] = $value }
elseif ($currentProfile) { $profiles[$currentProfile][$key] = $value }
}
}
return @{ Profiles = $profiles; Hosts = $hosts }
}
function Get-SshConfigBlock {
param(
[string]$HostAlias,
[hashtable]$HostEntry,
[string]$IdentityFile
)
@(
"# BEGIN Setup-SshAccess:$HostAlias"
"Host $HostAlias"
" HostName $($HostEntry.HostName)"
" User $($HostEntry.User)"
" Port $($HostEntry.Port)"
" IdentityFile $IdentityFile"
" IdentitiesOnly yes"
" AddKeysToAgent yes"
" StrictHostKeyChecking accept-new"
"# END Setup-SshAccess:$HostAlias"
) -join "`n"
}
function Update-SshConfig {
param(
[string]$ConfigPath,
[string]$HostAlias,
[string]$Block
)
$begin = "# BEGIN Setup-SshAccess:$HostAlias"
$end = "# END Setup-SshAccess:$HostAlias"
$existing = if (Test-Path -LiteralPath $ConfigPath) {
Get-Content -LiteralPath $ConfigPath -Raw
} else {
''
}
$pattern = "(?ms)^$([regex]::Escape($begin)).*?$([regex]::Escape($end))\r?\n?"
if ($existing -match $pattern) {
return ($existing -replace $pattern, ($Block + "`n"))
}
if ($existing -and -not $existing.EndsWith("`n")) {
$existing += "`n"
}
return $existing + $Block + "`n"
}
if (-not $HostsConfig) {
$HostsConfig = Join-Path $ConfigDir 'git-ssh-hosts.conf'
}
if (-not (Test-Path -LiteralPath $HostsConfig)) {
throw "Config not found: $HostsConfig"
}
$parsed = Read-GitSshHostsConfig -Path $HostsConfig
$sshDir = Join-Path $HOME '.ssh'
$sshConfigPath = Join-Path $sshDir 'config'
if (-not $WhatIf) {
New-Item -ItemType Directory -Path $sshDir -Force | Out-Null
}
$configText = if (Test-Path -LiteralPath $sshConfigPath) {
Get-Content -LiteralPath $sshConfigPath -Raw
} else {
''
}
$profileKeys = @{}
foreach ($profileName in $parsed.Profiles.Keys) {
$profile = $parsed.Profiles[$profileName]
$keyName = $profile.KeyName
if (-not $keyName -or $profileKeys.ContainsKey($keyName)) { continue }
$keyPath = Join-Path $sshDir $keyName
$comment = if ($profile.KeyComment) { $profile.KeyComment } else { $keyName }
if (-not (Test-Path -LiteralPath $keyPath)) {
Write-Step "Generating $keyName"
if ($WhatIf) {
Write-Warn "WhatIf: ssh-keygen -t ed25519 -f $keyPath"
}
else {
& ssh-keygen -t ed25519 -f $keyPath -N '""' -C $comment -q
Write-Ok "key $keyName created"
}
}
else {
Write-Ok "key $keyName exists"
}
$profileKeys[$keyName] = $profile
}
foreach ($hostAlias in ($parsed.Hosts.Keys | Sort-Object)) {
$hostEntry = $parsed.Hosts[$hostAlias]
$profileName = $hostEntry.Profile
if (-not $profileName -or -not $parsed.Profiles.ContainsKey($profileName)) {
throw "Host ${hostAlias}: unknown profile $profileName"
}
$profile = $parsed.Profiles[$profileName]
$identity = "~/.ssh/$($profile.KeyName)"
$block = Get-SshConfigBlock -HostAlias $hostAlias -HostEntry $hostEntry -IdentityFile $identity
if ($WhatIf) {
Write-Step "WhatIf: ssh config block for $hostAlias"
continue
}
$configText = Update-SshConfig -ConfigPath $sshConfigPath -HostAlias $hostAlias -Block $block
Write-Ok "ssh config: $hostAlias"
}
if (-not $WhatIf) {
Set-Content -LiteralPath $sshConfigPath -Value $configText.TrimEnd() -Encoding UTF8 -NoNewline
Add-Content -LiteralPath $sshConfigPath -Value '' -Encoding UTF8
Get-Service ssh-agent -ErrorAction SilentlyContinue | Where-Object { $_.Status -ne 'Running' } |
ForEach-Object { Start-Service ssh-agent | Out-Null }
foreach ($keyName in $profileKeys.Keys) {
$keyPath = Join-Path $sshDir $keyName
if (Test-Path -LiteralPath $keyPath) {
ssh-add $keyPath 2>$null | Out-Null
}
}
}
Write-Ok 'SSH setup complete'
+136
View File
@@ -0,0 +1,136 @@
#!/usr/bin/env bash
# Регистрирует публичные SSH-ключи в Gitea через API.
set -euo pipefail
CONFIG_DIR="${CONFIG_DIR:-$HOME/CursorRules/local}"
HOSTS_CONFIG="${HOSTS_CONFIG:-$CONFIG_DIR/git-ssh-hosts.conf}"
WHAT_IF="${WHAT_IF:-0}"
log_step() { printf '[*] %s\n' "$1"; }
log_ok() { printf '[+] %s\n' "$1"; }
log_warn() { printf '[!] %s\n' "$1"; }
if [[ ! -f "$HOSTS_CONFIG" ]]; then
echo "Config not found: $HOSTS_CONFIG" >&2
exit 1
fi
read_kv_file() {
local file="$1"
local line key value
while IFS= read -r line || [[ -n "$line" ]]; do
[[ "$line" =~ ^[[:space:]]*# ]] && continue
[[ "$line" != *=* ]] && continue
key="${line%%=*}"
value="${line#*=}"
key="${key%"${key##*[![:space:]]}"}"
value="${value#"${value%%[![:space:]]*}"}"
printf '%s=%s\n' "$key" "$value"
done < "$file"
}
declare -A PROFILE_KEYNAME PROFILE_GITEA
current_section=''
current_name=''
while IFS= read -r line || [[ -n "$line" ]]; do
line="${line%%#*}"
line="${line%"${line##*[![:space:]]}"}"
[[ -z "$line" ]] && continue
if [[ "$line" =~ ^\[profile:(.+)\]$ ]]; then
current_section='profile'
current_name="${BASH_REMATCH[1]}"
continue
fi
if [[ "$line" =~ ^\[host: ]]; then
current_section=''
continue
fi
[[ "$line" != *=* || "$current_section" != 'profile' ]] && continue
key="${line%%=*}"
value="${line#*=}"
key="${key%"${key##*[![:space:]]}"}"
value="${value#"${value%%[![:space:]]*}"}"
case "$key" in
KeyName) PROFILE_KEYNAME["$current_name"]="$value" ;;
GiteaConfig) PROFILE_GITEA["$current_name"]="$value" ;;
esac
done < "$HOSTS_CONFIG"
SSH_DIR="$HOME/.ssh"
MACHINE="$(hostname -s 2>/dev/null || hostname)"
declare -A REGISTERED
register_gitea_key() {
local cfg_file="$1"
local pub_path="$2"
local title="$3"
declare -A CFG=()
local kv key value
while IFS= read -r kv; do
key="${kv%%=*}"
value="${kv#*=}"
CFG["$key"]="$value"
done < <(read_kv_file "$cfg_file")
if [[ -z "${CFG[GiteaApiPassword]:-}" ]]; then
log_warn "$title : GiteaApiPassword empty — add key manually in Gitea UI"
cat "$pub_path"
return 0
fi
local base="${CFG[GiteaApiUrl]%/}"
local auth user pass pub body response
user="${CFG[GiteaApiUser]}"
pass="${CFG[GiteaApiPassword]}"
auth="$(printf '%s:%s' "$user" "$pass" | base64 -w0 2>/dev/null || printf '%s:%s' "$user" "$pass" | base64)"
pub="$(tr -d '\r\n' < "$pub_path")"
response="$(curl -fsS -H "Authorization: Basic $auth" "$base/api/v1/user/keys" 2>/dev/null || true)"
if [[ -n "$response" ]] && printf '%s' "$response" | grep -Fq "$pub"; then
log_ok "$title : key already registered"
return 0
fi
body="$(printf '{"title":"%s","key":"%s"}' "$title" "$pub")"
if [[ "$WHAT_IF" == '1' ]]; then
log_step "WhatIf: POST $base/api/v1/user/keys ($title)"
return 0
fi
curl -fsS -X POST -H "Authorization: Basic $auth" -H 'Content-Type: application/json' \
-d "$body" "$base/api/v1/user/keys" >/dev/null
log_ok "$title : registered"
}
for profile in "${!PROFILE_GITEA[@]}"; do
gitea_config="${PROFILE_GITEA[$profile]}"
[[ -z "$gitea_config" ]] && continue
key_name="${PROFILE_KEYNAME[$profile]}"
[[ -z "$key_name" || -n "${REGISTERED[$key_name]:-}" ]] && continue
REGISTERED["$key_name"]=1
gitea_path="$CONFIG_DIR/$gitea_config"
pub_path="$SSH_DIR/$key_name.pub"
if [[ ! -f "$gitea_path" ]]; then
log_warn "Gitea config not found: $gitea_path"
continue
fi
if [[ ! -f "$pub_path" ]]; then
log_warn "Public key not found: $pub_path (run setup-git-ssh.sh first)"
continue
fi
title="$MACHINE-$key_name"
log_step "Registering $key_name"
register_gitea_key "$gitea_path" "$pub_path" "$title"
done
log_ok 'Gitea SSH key registration done'
+160
View File
@@ -0,0 +1,160 @@
#!/usr/bin/env bash
# Генерирует SSH-ключи и ~/.ssh/config для Git-хостов из local/git-ssh-hosts.conf.
set -euo pipefail
CONFIG_DIR="${CONFIG_DIR:-$HOME/CursorRules/local}"
HOSTS_CONFIG="${HOSTS_CONFIG:-$CONFIG_DIR/git-ssh-hosts.conf}"
WHAT_IF="${WHAT_IF:-0}"
log_step() { printf '[*] %s\n' "$1"; }
log_ok() { printf '[+] %s\n' "$1"; }
log_warn() { printf '[!] %s\n' "$1"; }
if [[ ! -f "$HOSTS_CONFIG" ]]; then
echo "Config not found: $HOSTS_CONFIG" >&2
exit 1
fi
SSH_DIR="$HOME/.ssh"
SSH_CONFIG="$SSH_DIR/config"
mkdir -p "$SSH_DIR"
chmod 700 "$SSH_DIR"
declare -A PROFILE_KEYNAME PROFILE_COMMENT PROFILE_GITEA
declare -A HOST_HOSTNAME HOST_PORT HOST_USER HOST_PROFILE
current_section=''
current_name=''
while IFS= read -r line || [[ -n "$line" ]]; do
line="${line%%#*}"
line="${line%"${line##*[![:space:]]}"}"
[[ -z "$line" ]] && continue
if [[ "$line" =~ ^\[profile:(.+)\]$ ]]; then
current_section='profile'
current_name="${BASH_REMATCH[1]}"
continue
fi
if [[ "$line" =~ ^\[host:(.+)\]$ ]]; then
current_section='host'
current_name="${BASH_REMATCH[1]}"
HOST_HOSTNAME["$current_name"]=''
continue
fi
if [[ "$line" != *=* ]]; then
continue
fi
key="${line%%=*}"
value="${line#*=}"
key="${key%"${key##*[![:space:]]}"}"
value="${value#"${value%%[![:space:]]*}"}"
if [[ "$current_section" == 'profile' ]]; then
case "$key" in
KeyName) PROFILE_KEYNAME["$current_name"]="$value" ;;
KeyComment) PROFILE_COMMENT["$current_name"]="$value" ;;
GiteaConfig) PROFILE_GITEA["$current_name"]="$value" ;;
esac
elif [[ "$current_section" == 'host' ]]; then
case "$key" in
HostName) HOST_HOSTNAME["$current_name"]="$value" ;;
Port) HOST_PORT["$current_name"]="$value" ;;
User) HOST_USER["$current_name"]="$value" ;;
Profile) HOST_PROFILE["$current_name"]="$value" ;;
esac
fi
done < "$HOSTS_CONFIG"
declare -A SEEN_KEYS
for profile in "${!PROFILE_KEYNAME[@]}"; do
key_name="${PROFILE_KEYNAME[$profile]}"
[[ -z "$key_name" || -n "${SEEN_KEYS[$key_name]:-}" ]] && continue
SEEN_KEYS["$key_name"]=1
key_path="$SSH_DIR/$key_name"
comment="${PROFILE_COMMENT[$profile]:-$key_name}"
if [[ ! -f "$key_path" ]]; then
log_step "Generating $key_name"
if [[ "$WHAT_IF" == '1' ]]; then
log_warn "WhatIf: ssh-keygen -t ed25519 -f $key_path"
else
ssh-keygen -t ed25519 -f "$key_path" -N '' -C "$comment" -q
chmod 600 "$key_path"
log_ok "key $key_name created"
fi
else
log_ok "key $key_name exists"
fi
done
update_ssh_block() {
local host_alias="$1"
local block="$2"
local begin="# BEGIN Setup-SshAccess:$host_alias"
local end="# END Setup-SshAccess:$host_alias"
local tmp
tmp="$(mktemp)"
if [[ -f "$SSH_CONFIG" ]]; then
awk -v begin="$begin" -v end="$end" -v block="$block" '
$0 == begin { skip=1; next }
$0 == end { skip=0; next }
skip { next }
{ print }
END {
if (block != "") {
print block
}
}
' block="$block" "$SSH_CONFIG" > "$tmp"
else
printf '%s\n' "$block" > "$tmp"
fi
if [[ "$WHAT_IF" != '1' ]]; then
mv "$tmp" "$SSH_CONFIG"
chmod 600 "$SSH_CONFIG"
else
rm -f "$tmp"
fi
}
for host_alias in $(printf '%s\n' "${!HOST_HOSTNAME[@]}" | sort); do
profile="${HOST_PROFILE[$host_alias]:-}"
key_name="${PROFILE_KEYNAME[$profile]:-}"
if [[ -z "$profile" || -z "$key_name" ]]; then
echo "Host $host_alias: unknown profile" >&2
exit 1
fi
block="# BEGIN Setup-SshAccess:$host_alias
Host $host_alias
HostName ${HOST_HOSTNAME[$host_alias]}
User ${HOST_USER[$host_alias]}
Port ${HOST_PORT[$host_alias]}
IdentityFile ~/.ssh/$key_name
IdentitiesOnly yes
AddKeysToAgent yes
StrictHostKeyChecking accept-new
# END Setup-SshAccess:$host_alias"
if [[ "$WHAT_IF" == '1' ]]; then
log_step "WhatIf: ssh config block for $host_alias"
else
update_ssh_block "$host_alias" "$block"
log_ok "ssh config: $host_alias"
fi
done
if [[ "$WHAT_IF" != '1' ]]; then
eval "$(ssh-agent -s)" >/dev/null
for key_name in "${!SEEN_KEYS[@]}"; do
[[ -f "$SSH_DIR/$key_name" ]] && ssh-add "$SSH_DIR/$key_name" >/dev/null 2>&1 || true
done
fi
log_ok 'SSH setup complete'