From bdf405e2c179b84197751dc9b928ea3c27a8b8b0 Mon Sep 17 00:00:00 2001 From: PTah Date: Thu, 18 Jun 2026 10:01:11 +1000 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20bootstrap=20=D0=B4=D0=BB=D1=8F=20=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D1=8B=D1=85=20=D0=9F=D0=9A=20=D0=B8=20Linux-=D1=81=D0=BA=D1=80?= =?UTF-8?q?=D0=B8=D0=BF=D1=82=D1=8B.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SSH-ключи и Gitea API до clone Rules; init-cursor копирует из локального $HOME/CursorRules. --- README.md | 57 +++++++-- bootstrap.ps1 | 140 +++++++++++++++++++++ bootstrap.sh | 87 +++++++++++++ init-cursor.ps1 | 27 +++- init-cursor.sh | 85 +++++++++++++ local/git-ssh-hosts.conf | 52 ++++++++ local/git-user.conf | 2 + local/kalinamall-gitea.conf | 9 ++ scripts/Register-GiteaSshKeys.ps1 | 136 ++++++++++++++++++++ scripts/Set-GitRemotes.sh | 73 +++++++++++ scripts/Setup-GitSsh.ps1 | 196 +++++++++++++++++++++++++++++ scripts/register-gitea-ssh-keys.sh | 136 ++++++++++++++++++++ scripts/setup-git-ssh.sh | 160 +++++++++++++++++++++++ 13 files changed, 1147 insertions(+), 13 deletions(-) create mode 100644 bootstrap.ps1 create mode 100644 bootstrap.sh create mode 100644 init-cursor.sh create mode 100644 local/git-ssh-hosts.conf create mode 100644 local/git-user.conf create mode 100644 local/kalinamall-gitea.conf create mode 100644 scripts/Register-GiteaSshKeys.ps1 create mode 100644 scripts/Set-GitRemotes.sh create mode 100644 scripts/Setup-GitSsh.ps1 create mode 100644 scripts/register-gitea-ssh-keys.sh create mode 100644 scripts/setup-git-ssh.sh diff --git a/README.md b/README.md index fe67acd..350113d 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,42 @@ # Rules -Правила и скрипты для настройки нового компьютера: Cursor rules, git remotes, home Gitea. +Правила и скрипты для настройки нового компьютера: SSH-ключи, Cursor rules, git remotes, home Gitea. -## Первичная настройка +## Как это устроено + +| Этап | Скрипт | Что делает | +|------|--------|------------| +| Новый ПК | `bootstrap.ps1` / `bootstrap.sh` | SSH-ключи, регистрация в Gitea, `git clone` Rules → `$HOME/CursorRules` | +| Новый проект | `init-cursor` | Копирует rules **из локального** `$HOME/CursorRules` в текущий каталог | +| Обновить rules | `init-cursor -Update` | `git pull` в `$HOME/CursorRules`, затем копирование | + +`init-cursor` **не** тянет правила с remote напрямую — только из локального клона. Сначала нужен bootstrap (или ручной clone после настройки SSH). + +## Новый Windows-ПК + +На голом ПК нет доступа к приватному git — сначала скопируйте каталог Rules с рабочей машины: ```powershell -git clone https://git.kalinamall.ru/PapaTramp/Rules $HOME\CursorRules +scp -r user@working-pc:~/CursorRules $env:TEMP\rules-bootstrap +cd $env:TEMP\rules-bootstrap +.\bootstrap.ps1 ``` -Добавьте `init-cursor` в профиль PowerShell (один раз): +Bootstrap: -```powershell -. $HOME\CursorRules\init-cursor.ps1 +1. `git config` user.name / user.email (`local/git-user.conf`) +2. Генерирует SSH-ключи, пишет `~/.ssh/config` (`scripts/Setup-GitSsh.ps1`) +3. Регистрирует pubkey в Gitea API (`scripts/Register-GiteaSshKeys.ps1`) +4. `git clone ssh://git@git.kalinamall.lan:2222/PapaTramp/Rules.git` → `$HOME\CursorRules` +5. Добавляет `init-cursor` в PowerShell profile + +## Новый Linux-хост + +```bash +scp -r user@working-pc:~/CursorRules /tmp/rules-bootstrap +cd /tmp/rules-bootstrap +chmod +x bootstrap.sh scripts/*.sh +./bootstrap.sh ``` ## В каждом проекте @@ -19,6 +44,24 @@ git clone https://git.kalinamall.ru/PapaTramp/Rules $HOME\CursorRules ```powershell cd C:\path\to\repo init-cursor +init-cursor -Update # подтянуть свежие rules из remote ``` -Скрипт копирует `.cursorignore` и `.cursor/rules/*.mdc`, при необходимости дополняет `.gitignore`, настраивает remotes (`home`, `github`, `kalinamall`) и создаёт репозиторий на home Gitea через API. +```bash +cd ~/projects/myrepo +init-cursor +init-cursor --update +``` + +Скрипт копирует `.cursorignore` и `.cursor/rules/*.mdc`, дополняет `.gitignore`, настраивает remotes (`home`, `github`, `kalinamall`) и создаёт репозиторий на home Gitea через API. + +## Конфиги (local/) + +| Файл | Назначение | +|------|------------| +| `git-ssh-hosts.conf` | Хосты и профили SSH-ключей | +| `git-user.conf` | `user.name` / `user.email` | +| `kalinamall-gitea.conf` | API kalinamall (для регистрации SSH-ключа) | +| `papatramp-gitea.conf` | API home Gitea + Ensure-GiteaHomeRepo | + +Заполните `GiteaApiPassword` в `kalinamall-gitea.conf` (приватный репозиторий). diff --git a/bootstrap.ps1 b/bootstrap.ps1 new file mode 100644 index 0000000..cdc569c --- /dev/null +++ b/bootstrap.ps1 @@ -0,0 +1,140 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Первичная настройка нового компьютера: SSH, Gitea keys, clone Rules, init-cursor в профиль. + +.DESCRIPTION + 1. git user.name / user.email + 2. SSH-ключи и ~/.ssh/config + 3. Регистрация pubkey в Gitea (kalinamall, papatramp) + 4. git clone Rules -> $HOME\CursorRules + 5. Подключение init-cursor в PowerShell profile + + На голом ПК скопируйте каталог Rules с рабочей машины (scp/USB), затем: + .\bootstrap.ps1 + +.EXAMPLE + .\bootstrap.ps1 +#> +[CmdletBinding()] +param( + [string]$RulesDest = '', + [string]$CloneUrl = 'ssh://git@git.kalinamall.lan:2222/PapaTramp/Rules.git', + [switch]$SkipClone, + [switch]$SkipProfile, + [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 } + +$root = $PSScriptRoot +$configDir = Join-Path $root 'local' +if (-not $RulesDest) { + $RulesDest = Join-Path $HOME 'CursorRules' +} + +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 +} + +Write-Step 'Bootstrap: new machine setup' + +$userCfgPath = Join-Path $configDir 'git-user.conf' +if (Test-Path -LiteralPath $userCfgPath) { + $userCfg = Read-KeyValueFile -Path $userCfgPath + if ($userCfg.UserName) { + git config --global user.name $userCfg.UserName | Out-Null + Write-Ok "git user.name = $($userCfg.UserName)" + } + if ($userCfg.UserEmail) { + git config --global user.email $userCfg.UserEmail | Out-Null + Write-Ok "git user.email = $($userCfg.UserEmail)" + } +} + +$setupArgs = @{ + ConfigDir = $configDir + WhatIf = $WhatIf.IsPresent +} +& (Join-Path $root 'scripts\Setup-GitSsh.ps1') @setupArgs + +$registerArgs = @{ + ConfigDir = $configDir + WhatIf = $WhatIf.IsPresent +} +& (Join-Path $root 'scripts\Register-GiteaSshKeys.ps1') @registerArgs + +if (-not $WhatIf) { + Write-Step 'Testing git@git.kalinamall.lan' + ssh -T -o BatchMode=yes -o ConnectTimeout=10 git@git.kalinamall.lan 2>&1 | ForEach-Object { Write-Host $_ } + if ($LASTEXITCODE -ne 0 -and $LASTEXITCODE -ne 1) { + Write-Warn 'SSH test failed — check VPN/LAN and Gitea SSH keys' + } +} + +if (-not $SkipClone) { + if ($WhatIf) { + Write-Step "WhatIf: clone $CloneUrl -> $RulesDest" + } + elseif (Test-Path (Join-Path $RulesDest '.git')) { + Write-Step "Updating $RulesDest" + git -C $RulesDest pull --ff-only + Write-Ok 'CursorRules updated' + } + else { + if (Test-Path -LiteralPath $RulesDest) { + throw "Path exists and is not a git repo: $RulesDest" + } + Write-Step "Cloning $CloneUrl" + git clone $CloneUrl $RulesDest + Write-Ok "Cloned to $RulesDest" + } +} + +if (-not $SkipProfile) { + $profileHook = ". `"$RulesDest\init-cursor.ps1`"" + $profilePath = $PROFILE.CurrentUserAllHosts + if (-not $profilePath) { $profilePath = $PROFILE.CurrentUserCurrentHost } + + if ($WhatIf) { + Write-Step "WhatIf: add to profile $profilePath" + } + else { + $profileDir = Split-Path -Parent $profilePath + if (-not (Test-Path -LiteralPath $profileDir)) { + New-Item -ItemType Directory -Path $profileDir -Force | Out-Null + } + + $profileText = if (Test-Path -LiteralPath $profilePath) { + Get-Content -LiteralPath $profilePath -Raw + } else { + '' + } + + if ($profileText -notmatch [regex]::Escape($profileHook)) { + if ($profileText -and -not $profileText.EndsWith("`n")) { $profileText += "`n" } + $profileText += $profileHook + "`n" + Set-Content -LiteralPath $profilePath -Value $profileText.TrimEnd() -Encoding UTF8 + Write-Ok "Profile updated: $profilePath" + } + else { + Write-Ok 'init-cursor already in profile' + } + } +} + +Write-Host '' +Write-Ok 'Bootstrap complete. Open a new shell, then in a project: init-cursor' diff --git a/bootstrap.sh b/bootstrap.sh new file mode 100644 index 0000000..fd6dc85 --- /dev/null +++ b/bootstrap.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# Первичная настройка нового Linux-хоста: SSH, Gitea keys, clone Rules, init-cursor в shell profile. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CONFIG_DIR="$ROOT/local" +RULES_DEST="${RULES_DEST:-$HOME/CursorRules}" +CLONE_URL="${CLONE_URL:-ssh://git@git.kalinamall.lan:2222/PapaTramp/Rules.git}" +SKIP_CLONE="${SKIP_CLONE:-0}" +SKIP_PROFILE="${SKIP_PROFILE:-0}" +WHAT_IF="${WHAT_IF:-0}" + +log_step() { printf '[*] %s\n' "$1"; } +log_ok() { printf '[+] %s\n' "$1"; } +log_warn() { printf '[!] %s\n' "$1"; } + +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" +} + +log_step 'Bootstrap: new machine setup' + +user_cfg="$CONFIG_DIR/git-user.conf" +if [[ -f "$user_cfg" ]]; then + while IFS= read -r kv; do + key="${kv%%=*}" + value="${kv#*=}" + case "$key" in + UserName) git config --global user.name "$value"; log_ok "git user.name = $value" ;; + UserEmail) git config --global user.email "$value"; log_ok "git user.email = $value" ;; + esac + done < <(read_kv_file "$user_cfg") +fi + +export CONFIG_DIR WHAT_IF +bash "$ROOT/scripts/setup-git-ssh.sh" +bash "$ROOT/scripts/register-gitea-ssh-keys.sh" + +if [[ "$WHAT_IF" != '1' ]]; then + log_step 'Testing git@git.kalinamall.lan' + ssh -T -o BatchMode=yes -o ConnectTimeout=10 git@git.kalinamall.lan || true +fi + +if [[ "$SKIP_CLONE" != '1' ]]; then + if [[ "$WHAT_IF" == '1' ]]; then + log_step "WhatIf: clone $CLONE_URL -> $RULES_DEST" + elif [[ -d "$RULES_DEST/.git" ]]; then + log_step "Updating $RULES_DEST" + git -C "$RULES_DEST" pull --ff-only + log_ok 'CursorRules updated' + else + [[ -e "$RULES_DEST" ]] && { echo "Path exists and is not a git repo: $RULES_DEST" >&2; exit 1; } + log_step "Cloning $CLONE_URL" + git clone "$CLONE_URL" "$RULES_DEST" + log_ok "Cloned to $RULES_DEST" + fi +fi + +if [[ "$SKIP_PROFILE" != '1' ]]; then + profile_hook="source \"$RULES_DEST/init-cursor.sh\"" + for profile in "$HOME/.bashrc" "$HOME/.zshrc"; do + [[ -f "$profile" ]] || continue + if ! grep -Fq "$profile_hook" "$profile" 2>/dev/null; then + if [[ "$WHAT_IF" == '1' ]]; then + log_step "WhatIf: add to $profile" + else + printf '\n%s\n' "$profile_hook" >> "$profile" + log_ok "Profile updated: $profile" + fi + else + log_ok "init-cursor already in $profile" + fi + done +fi + +printf '\n' +log_ok 'Bootstrap complete. Open a new shell, then in a project: init-cursor' diff --git a/init-cursor.ps1 b/init-cursor.ps1 index 9b4da44..a84955f 100644 --- a/init-cursor.ps1 +++ b/init-cursor.ps1 @@ -1,21 +1,23 @@ #Requires -Version 5.1 <# .SYNOPSIS - Копирует Cursor rules в текущий каталог и настраивает git remotes. + Копирует Cursor rules из локального $HOME\CursorRules в текущий проект. .DESCRIPTION - 1. .cursorignore из $HOME\CursorRules - 2. .cursor/rules/*.mdc - 3. В git-репо: Set-GitRemotes.ps1 + Ensure-GiteaHomeRepo.ps1 (home Gitea) + Источник — локальный клон репозитория Rules ($HOME\CursorRules), не remote напрямую. + На новом ПК сначала: bootstrap.ps1 / bootstrap.sh (SSH + clone Rules). + Обновить правила из remote: init-cursor -Update .EXAMPLE cd C:\Users\papat\Projects\MyRepo init-cursor + init-cursor -Update #> function init-cursor { [CmdletBinding()] param( - [switch]$SkipGitRemotes + [switch]$SkipGitRemotes, + [switch]$Update ) $source = Join-Path $HOME 'CursorRules' @@ -23,10 +25,23 @@ function init-cursor { $scripts = Join-Path $source 'scripts' if (-not (Test-Path $source)) { - Write-Host "Error: $source not found" -ForegroundColor Red + Write-Host "Error: $source not found — run bootstrap.ps1 on this machine first" -ForegroundColor Red return } + if ($Update) { + if (Test-Path (Join-Path $source '.git')) { + Write-Host '[*] Updating CursorRules from remote...' -ForegroundColor Cyan + git -C $source pull --ff-only + if ($LASTEXITCODE -ne 0) { + Write-Host '[!] git pull failed in CursorRules' -ForegroundColor Yellow + } + else { + Write-Host '[+] CursorRules updated' -ForegroundColor Green + } + } + } + $cursorIgnore = Join-Path $source '.cursorignore' if (Test-Path $cursorIgnore) { Copy-Item $cursorIgnore . -Force diff --git a/init-cursor.sh b/init-cursor.sh new file mode 100644 index 0000000..e8e1169 --- /dev/null +++ b/init-cursor.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# Копирует Cursor rules из локального $HOME/CursorRules в текущий проект. +init-cursor() { + local source="$HOME/CursorRules" + local rules_src="$source/.cursor/rules" + local scripts="$source/scripts" + local update=0 + local skip_git_remotes=0 + + while [[ $# -gt 0 ]]; do + case "$1" in + -Update|--update) update=1; shift ;; + -SkipGitRemotes|--skip-git-remotes) skip_git_remotes=1; shift ;; + *) shift ;; + esac + done + + if [[ ! -d "$source" ]]; then + echo "Error: $source not found — run bootstrap.sh on this machine first" >&2 + return 1 + fi + + if [[ "$update" == '1' && -d "$source/.git" ]]; then + echo '[*] Updating CursorRules from remote...' + if git -C "$source" pull --ff-only; then + echo '[+] CursorRules updated' + else + echo '[!] git pull failed in CursorRules' >&2 + fi + fi + + if [[ -f "$source/.cursorignore" ]]; then + cp -f "$source/.cursorignore" . + echo '[+] .cursorignore copied' + fi + + if [[ -d "$rules_src" ]]; then + mkdir -p ./.cursor/rules + cp -f "$rules_src"/*.mdc ./.cursor/rules/ + echo '[+] .cursor/rules (*.mdc) copied' + fi + + if [[ -d .git ]]; then + local gitignore='.gitignore' + for line in '.cursor/' '.cursorignore'; do + if [[ ! -f "$gitignore" ]]; then + printf '%s\n' "$line" > "$gitignore" + echo "[+] .gitignore created ($line)" + elif ! grep -Fxq "$line" "$gitignore" 2>/dev/null; then + printf '%s\n' "$line" >> "$gitignore" + echo "[+] .gitignore updated ($line)" + fi + done + fi + + if [[ "$skip_git_remotes" == '1' ]]; then + echo '[!] Git remotes skipped' + return 0 + fi + + if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then + echo '[!] Not a git repo — remotes skipped' + return 0 + fi + + if [[ -f "$scripts/Set-GitRemotes.sh" ]]; then + echo '[*] Configuring git remotes...' + bash "$scripts/Set-GitRemotes.sh" || true + elif [[ -f "$scripts/Set-GitRemotes.ps1" ]]; then + echo '[!] Set-GitRemotes.ps1 only; install bash script or pwsh' >&2 + fi + + if [[ -f "$scripts/Ensure-GiteaHomeRepo.ps1" ]]; then + local repo_name + repo_name="$(basename "$(git rev-parse --show-toplevel)")" + echo "[*] Ensuring home Gitea repo: $repo_name" + if command -v pwsh >/dev/null 2>&1; then + pwsh -NoProfile -File "$scripts/Ensure-GiteaHomeRepo.ps1" -RepoName "$repo_name" || true + elif command -v powershell >/dev/null 2>&1; then + powershell -NoProfile -ExecutionPolicy Bypass -File "$scripts/Ensure-GiteaHomeRepo.ps1" -RepoName "$repo_name" || true + else + echo '[!] PowerShell not found — skip Ensure-GiteaHomeRepo.ps1' >&2 + fi + fi +} diff --git a/local/git-ssh-hosts.conf b/local/git-ssh-hosts.conf new file mode 100644 index 0000000..32dd4c4 --- /dev/null +++ b/local/git-ssh-hosts.conf @@ -0,0 +1,52 @@ +# Профили SSH-ключей и хосты Git. GiteaConfig — файл в local/ для регистрации pubkey через API. + +[profile:kalinamall-git] +KeyName=id_ed25519_git_kalinamall +KeyComment=papatramp@kalinamall +GiteaConfig=kalinamall-gitea.conf + +[host:git.kalinamall.ru] +HostName=git.kalinamall.ru +Port=2222 +User=git +Profile=kalinamall-git + +[host:git.kalinamall.lan] +HostName=192.168.160.129 +Port=2222 +User=git +Profile=kalinamall-git + +[profile:papatramp-git] +KeyName=id_ed25519_git_papatramp +KeyComment=papatramp@papatramp.ru +GiteaConfig=papatramp-gitea.conf + +[host:git.papatramp.ru] +HostName=git.papatramp.ru +Port=2222 +User=git +Profile=papatramp-git + +[host:git.papatramp.lan] +HostName=192.168.128.48 +Port=2222 +User=git +Profile=papatramp-git + +[host:git.papatramp-haproxy] +HostName=192.168.128.65 +Port=2222 +User=git +Profile=papatramp-git + +[profile:github] +KeyName=id_ed25519_github +KeyComment=papatramp@gmail.com +GiteaConfig= + +[host:github.com] +HostName=github.com +Port=22 +User=git +Profile=github diff --git a/local/git-user.conf b/local/git-user.conf new file mode 100644 index 0000000..2ad7cb4 --- /dev/null +++ b/local/git-user.conf @@ -0,0 +1,2 @@ +UserName=PTah +UserEmail=papatramp@gmail.com diff --git a/local/kalinamall-gitea.conf b/local/kalinamall-gitea.conf new file mode 100644 index 0000000..79ce465 --- /dev/null +++ b/local/kalinamall-gitea.conf @@ -0,0 +1,9 @@ +Type=gitea +GiteaHost=git.kalinamall.ru +GiteaPort=2222 +GiteaWebUrl=https://git.kalinamall.ru +GiteaApiUrl=http://192.168.160.129:3000 +GiteaApiUser=papatramp +GiteaApiPassword= +KeyName=id_ed25519_git_kalinamall +KeyComment=papatramp@kalinamall diff --git a/scripts/Register-GiteaSshKeys.ps1 b/scripts/Register-GiteaSshKeys.ps1 new file mode 100644 index 0000000..2feecbe --- /dev/null +++ b/scripts/Register-GiteaSshKeys.ps1 @@ -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' diff --git a/scripts/Set-GitRemotes.sh b/scripts/Set-GitRemotes.sh new file mode 100644 index 0000000..15586a2 --- /dev/null +++ b/scripts/Set-GitRemotes.sh @@ -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 diff --git a/scripts/Setup-GitSsh.ps1 b/scripts/Setup-GitSsh.ps1 new file mode 100644 index 0000000..40c29fe --- /dev/null +++ b/scripts/Setup-GitSsh.ps1 @@ -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' diff --git a/scripts/register-gitea-ssh-keys.sh b/scripts/register-gitea-ssh-keys.sh new file mode 100644 index 0000000..cda5b9b --- /dev/null +++ b/scripts/register-gitea-ssh-keys.sh @@ -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' diff --git a/scripts/setup-git-ssh.sh b/scripts/setup-git-ssh.sh new file mode 100644 index 0000000..dc14a72 --- /dev/null +++ b/scripts/setup-git-ssh.sh @@ -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'