From 6065b163e2d184a00403c5dfd319031b7920cee2 Mon Sep 17 00:00:00 2001 From: PTah Date: Tue, 14 Jul 2026 20:43:31 +1000 Subject: [PATCH] chore(home): mirror from kalinamall (c53704a) with papatramp URLs --- .editorconfig | 21 +++ .gitattributes | 13 ++ .gitignore | 2 + README.md | 144 +++++++++++++++++++ bootstrap.ps1 | 28 ++++ bootstrap.sh | 13 ++ init-cursor.ps1 | 173 +++++++++++++++++++++++ init-cursor.sh | 143 +++++++++++++++++++ install.ps1 | 65 +++++++++ install.sh | 62 ++++++++ local/git-ssh-hosts.conf | 52 +++++++ local/git-user.conf | 2 + local/kalinamall-gitea.conf | 9 ++ local/papatramp-gitea.conf | 11 ++ scripts/Ensure-GiteaHomeRepo.ps1 | 63 +++++++++ scripts/Invoke-MachineBootstrap.ps1 | 210 ++++++++++++++++++++++++++++ scripts/Publish-GithubOrphan.ps1 | 144 +++++++++++++++++++ scripts/Push-HomeMirror.ps1 | 60 ++++++++ scripts/Register-GiteaSshKeys.ps1 | 136 ++++++++++++++++++ scripts/Rewrite-GitHostUrls.ps1 | 91 ++++++++++++ scripts/Set-GitRemotes.ps1 | 132 +++++++++++++++++ scripts/Set-GitRemotes.sh | 110 +++++++++++++++ scripts/Setup-GitSsh.ps1 | 192 +++++++++++++++++++++++++ scripts/invoke-machine-bootstrap.sh | 128 +++++++++++++++++ scripts/publish-to-github.sh | 92 ++++++++++++ scripts/push-home-mirror.sh | 33 +++++ scripts/register-gitea-ssh-keys.sh | 136 ++++++++++++++++++ scripts/rewrite-git-host-urls.sh | 85 +++++++++++ scripts/setup-git-ssh.sh | 198 ++++++++++++++++++++++++++ 29 files changed, 2548 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 README.md create mode 100644 bootstrap.ps1 create mode 100644 bootstrap.sh create mode 100644 init-cursor.ps1 create mode 100644 init-cursor.sh create mode 100644 install.ps1 create mode 100644 install.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 local/papatramp-gitea.conf create mode 100644 scripts/Ensure-GiteaHomeRepo.ps1 create mode 100644 scripts/Invoke-MachineBootstrap.ps1 create mode 100644 scripts/Publish-GithubOrphan.ps1 create mode 100644 scripts/Push-HomeMirror.ps1 create mode 100644 scripts/Register-GiteaSshKeys.ps1 create mode 100644 scripts/Rewrite-GitHostUrls.ps1 create mode 100644 scripts/Set-GitRemotes.ps1 create mode 100644 scripts/Set-GitRemotes.sh create mode 100644 scripts/Setup-GitSsh.ps1 create mode 100644 scripts/invoke-machine-bootstrap.sh create mode 100644 scripts/publish-to-github.sh create mode 100644 scripts/push-home-mirror.sh create mode 100644 scripts/register-gitea-ssh-keys.sh create mode 100644 scripts/rewrite-git-host-urls.sh create mode 100644 scripts/setup-git-ssh.sh diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..55e9303 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true + +[*.ps1] +charset = utf-8-bom +end_of_line = crlf + +[*.psm1] +charset = utf-8-bom +end_of_line = crlf + +[*.sh] +charset = utf-8 +end_of_line = lf + +[*.md] +end_of_line = lf diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..609182e --- /dev/null +++ b/.gitattributes @@ -0,0 +1,13 @@ +* text=auto + +*.ps1 text eol=crlf +*.psm1 text eol=crlf +*.psd1 text eol=crlf + +*.sh text eol=lf +*.bash text eol=lf + +*.md text eol=lf +*.json text eol=lf +*.yml text eol=lf +*.yaml text eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bbf8145 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.cursor/ +.cursorignore diff --git a/README.md b/README.md new file mode 100644 index 0000000..2bc642e --- /dev/null +++ b/README.md @@ -0,0 +1,144 @@ +# Rules + +Скрипты для Cursor rules, SSH и git remotes (`kalinamall`, `home`, `github`). + +## Быстро: что запускать + +| Ситуация | Windows | Mac / Linux | +|----------|---------|-------------| +| **Новый комп** (ничего не настроено) | Скопировать `install.ps1` с рабочего ПК → запустить в каталоге проекта | То же с `install.sh` | +| **Комп уже настраивали** (`~/CursorRules` есть) | `init-cursor` в каталоге проекта | `init-cursor` | +| **Только обновить rules** | `init-cursor -Update` | `init-cursor --update` | + +Скрипты **сами** проверяют SSH, clone, ключи и remotes. Вручную выбирать SSH/HTTPS не нужно. + +--- + +## Новый компьютер + +Нужен только **git**. Repo приватный — `curl`/`irm` по raw-URL **не работают**. + +### 1. Скопировать один файл с рабочей машины + +```powershell +# Windows — с другого ПК (пример) +scp user@work-pc:CursorRules/install.ps1 $env:TEMP\ +``` + +```bash +# Mac — с другого ПК +scp user@work-pc:CursorRules/install.sh /tmp/ +``` + +USB, общая папка — тоже подходит. + +### 2. Запустить в каталоге **вашего проекта** + +```powershell +# Windows +cd C:\path\to\your-repo +& $env:TEMP\install.ps1 +``` + +```bash +# Mac +cd ~/projects/your-repo +bash /tmp/install.sh +``` + +Скрипт сам: +1. Клонирует Rules в `~/CursorRules` (SSH → если не вышло, HTTPS с логином) +2. Настраивает SSH-ключи и Gitea API (если ещё нет) +3. Копирует rules в текущий проект, настраивает remotes +4. Добавляет `init-cursor` в профиль shell + +--- + +## Уже настроенный компьютер + +```powershell +cd C:\path\to\your-repo +init-cursor +``` + +```bash +cd ~/projects/your-repo +init-cursor +``` + +Если команда не найдена — один раз: + +```powershell +. $HOME\CursorRules\init-cursor.ps1 # Windows +``` + +```bash +source ~/CursorRules/init-cursor.sh # Mac +``` + +--- + +## Обновить rules / скрипты + +```powershell +init-cursor -Update # Windows +``` + +```bash +init-cursor --update # Mac +``` + +Обновит `$HOME/CursorRules` с Gitea и перекопирует rules в проект. + +--- + +## Git remotes (в каждом проекте) + +| Remote | Куда | +|--------|------| +| `kalinamall` | git.kalinamall.ru | +| `home` | git.papatramp.ru | +| `github` | github.com/PTah | + +Push: `git push kalinamall` + +## Push по remotes (важное) + +| Remote | Как пушить | Ссылки в файлах | +|--------|------------|-----------------| +| `kalinamall` | `git push kalinamall main` (эталон) | git.kalinamall.ru | +| `home` | `scripts/Push-HomeMirror.ps1` / `scripts/push-home-mirror.sh` | git.papatramp.ru | +| `github` | `scripts/Publish-GithubOrphan.ps1` / `scripts/publish-to-github.sh` | github.com/PTah + hard sanitize | + +Рабочее дерево и обычные коммиты всегда под **kalinamall**. На home/github — зеркала через скрипты (force). Rule: `remotes-mirror-and-sanitize.mdc` (копируется `init-cursor`). + +--- + +## Кодировка файлов (Windows / Linux) + +Правило **`file-encoding`** (`.cursor/rules/file-encoding.mdc`, `alwaysApply: true`): + +| Среда | Расширения | Кодировка | Переносы | +|-------|------------|-----------|----------| +| Windows / PowerShell | `.ps1`, `.psm1` | UTF-8 **with BOM** | CRLF | +| Linux / shell | `.sh` | UTF-8 **без BOM** | LF | +| Runtime JSON/spool (PS → SAC/API) | на диске | UTF-8 **без BOM** | — | + +В репозиториях проектов — `.editorconfig` и `.gitattributes` (шаблоны есть в этом repo). + +После `init-cursor --update` правило попадает во все проекты. + +--- + +## Конфиги (`local/`) + +| Файл | Назначение | +|------|------------| +| `git-ssh-hosts.conf` | SSH-хосты и ключи | +| `git-user.conf` | git user.name / email | +| `kalinamall-gitea.conf` | API kalinamall | +| `papatramp-gitea.conf` | API home Gitea | + +## Подключения на новом ПК + +Если SSH/Gitea/GitHub/LAN ещё не настроены — см. rule **`machine-connectivity-bootstrap`** и `install.ps1` / `install.sh`. diff --git a/bootstrap.ps1 b/bootstrap.ps1 new file mode 100644 index 0000000..e6a17b0 --- /dev/null +++ b/bootstrap.ps1 @@ -0,0 +1,28 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Обёртка над Invoke-MachineBootstrap.ps1 (совместимость). +#> +[CmdletBinding()] +param( + [string]$RulesDest = '', + [switch]$SkipClone, + [switch]$SkipProfile, + [switch]$WhatIf +) + +$root = $PSScriptRoot +if (-not $RulesDest) { + $RulesDest = Join-Path $HOME 'CursorRules' +} + +& (Join-Path $root 'scripts\Invoke-MachineBootstrap.ps1') @{ + Root = $root + RulesDest = $RulesDest + SkipClone = $SkipClone.IsPresent + SkipProfile = $SkipProfile.IsPresent + WhatIf = $WhatIf.IsPresent +} + +Write-Host '' +Write-Host '[+] Bootstrap complete. In a project run: init-cursor' -ForegroundColor Green diff --git a/bootstrap.sh b/bootstrap.sh new file mode 100644 index 0000000..f22de98 --- /dev/null +++ b/bootstrap.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# Обёртка над invoke-machine-bootstrap.sh (совместимость). +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +RULES_DEST="${RULES_DEST:-$HOME/CursorRules}" +SKIP_CLONE="${SKIP_CLONE:-0}" +SKIP_PROFILE="${SKIP_PROFILE:-0}" + +ROOT="$ROOT" RULES_DEST="$RULES_DEST" SKIP_CLONE="$SKIP_CLONE" SKIP_PROFILE="$SKIP_PROFILE" \ + bash "$ROOT/scripts/invoke-machine-bootstrap.sh" + +printf '\n[+] Bootstrap complete. In a project run: init-cursor\n' diff --git a/init-cursor.ps1 b/init-cursor.ps1 new file mode 100644 index 0000000..b82212e --- /dev/null +++ b/init-cursor.ps1 @@ -0,0 +1,173 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Инициализация Cursor в текущем проекте; на новом ПК сначала настраивает git/SSH. + +.DESCRIPTION + Один вызов init-cursor: + - если машина не настроена: SSH, Gitea keys, clone Rules в $HOME\CursorRules + - затем копирует rules в текущий репозиторий и настраивает remotes + + Первый запуск (приватный repo — через git clone, см. README): + git clone --depth 1 ssh://git@git.papatramp.ru:2222/PapaTramp/Rules.git $HOME\CursorRules + & $HOME\CursorRules\install.ps1 + +.EXAMPLE + cd C:\Users\papat\Projects\MyRepo + init-cursor + init-cursor -Update +#> +function Get-CursorRulesDest { + return (Join-Path $HOME 'CursorRules') +} + +function Get-CursorBootstrapRoot { + param([string]$RulesDest) + + $scriptRoot = $PSScriptRoot + if (Test-Path (Join-Path $scriptRoot 'scripts\Invoke-MachineBootstrap.ps1')) { + return $scriptRoot + } + if (Test-Path (Join-Path $RulesDest 'local\git-ssh-hosts.conf')) { + return $RulesDest + } + if (Test-Path (Join-Path $scriptRoot 'local\git-ssh-hosts.conf')) { + return $scriptRoot + } + return $null +} + +function Initialize-CursorRulesEnvironment { + [CmdletBinding()] + param( + [switch]$Update + ) + + $rulesDest = Get-CursorRulesDest + $bootstrapRoot = Get-CursorBootstrapRoot -RulesDest $rulesDest + + if (-not $bootstrapRoot) { + Write-Host 'Error: CursorRules not found. Run install.ps1 from Gitea first:' -ForegroundColor Red + Write-Host ' git clone --depth 1 ssh://git@git.papatramp.ru:2222/PapaTramp/Rules.git $HOME\CursorRules' -ForegroundColor Yellow + Write-Host ' & $HOME\CursorRules\install.ps1' -ForegroundColor Yellow + return $false + } + + $bootstrapScript = Join-Path $bootstrapRoot 'scripts\Invoke-MachineBootstrap.ps1' + if (-not (Test-Path -LiteralPath $bootstrapScript)) { + Write-Host "Error: bootstrap script not found: $bootstrapScript" -ForegroundColor Red + return $false + } + + $repoReady = Test-Path (Join-Path $rulesDest '.git') + $skipClone = $repoReady + + & $bootstrapScript -Root $bootstrapRoot -RulesDest $rulesDest -SkipClone:$skipClone + + if ($Update -and $repoReady) { + Write-Host '[*] Updating CursorRules from remote...' -ForegroundColor Cyan + git -C $rulesDest pull --ff-only + if ($LASTEXITCODE -ne 0) { + Write-Host '[!] git pull failed in CursorRules' -ForegroundColor Yellow + } + else { + Write-Host '[+] CursorRules updated' -ForegroundColor Green + } + } + + return $true +} + +function Install-CursorInCurrentProject { + [CmdletBinding()] + param( + [switch]$SkipGitRemotes + ) + + $source = Get-CursorRulesDest + $rulesSrc = Join-Path $source '.cursor\rules' + $scripts = Join-Path $source 'scripts' + + $cursorIgnore = Join-Path $source '.cursorignore' + if (Test-Path $cursorIgnore) { + Copy-Item $cursorIgnore . -Force + Write-Host '[+] .cursorignore copied' -ForegroundColor Green + } + + if (Test-Path $rulesSrc) { + New-Item -ItemType Directory -Path '.\.cursor\rules' -Force | Out-Null + Copy-Item (Join-Path $rulesSrc '*.mdc') '.\.cursor\rules\' -Force + Write-Host '[+] .cursor/rules (*.mdc) copied' -ForegroundColor Green + } + + $gitIgnorePath = Join-Path (Get-Location) '.gitignore' + $ignoreLines = @('.cursor/', '.cursorignore') + if (Test-Path (Join-Path (Get-Location) '.git')) { + if (-not (Test-Path $gitIgnorePath)) { + Set-Content -Path $gitIgnorePath -Value ($ignoreLines -join "`n") -Encoding UTF8 + Write-Host '[+] .gitignore created (.cursor/, .cursorignore)' -ForegroundColor Green + } + else { + $existing = Get-Content $gitIgnorePath -Raw + $added = @() + foreach ($line in $ignoreLines) { + if ($existing -notmatch "(?m)^$([regex]::Escape($line))\s*$") { + Add-Content -Path $gitIgnorePath -Value $line + $added += $line + } + } + if ($added.Count -gt 0) { + Write-Host "[+] .gitignore updated: $($added -join ', ')" -ForegroundColor Green + } + } + } + + if ($SkipGitRemotes) { + Write-Host '[!] Git remotes skipped (-SkipGitRemotes)' -ForegroundColor Yellow + return + } + + git rev-parse --is-inside-work-tree 2>$null | Out-Null + if ($LASTEXITCODE -ne 0) { + Write-Host '[!] Not a git repo - remotes skipped' -ForegroundColor Yellow + return + } + + $setRemotes = Join-Path $scripts 'Set-GitRemotes.ps1' + if (Test-Path $setRemotes) { + Write-Host '[*] Configuring git remotes...' -ForegroundColor Cyan + & powershell -NoProfile -ExecutionPolicy Bypass -File $setRemotes + } + + $ensureRepo = Join-Path $scripts 'Ensure-GiteaHomeRepo.ps1' + if (Test-Path $ensureRepo) { + $repoName = Split-Path -Leaf (git rev-parse --show-toplevel) + Write-Host "[*] Ensuring home Gitea repo: $repoName" -ForegroundColor Cyan + try { + & powershell -NoProfile -ExecutionPolicy Bypass -File $ensureRepo -RepoName $repoName + } + catch { + Write-Host "[!] home Gitea ensure failed: $($_.Exception.Message)" -ForegroundColor Yellow + } + } +} + +function init-cursor { + [CmdletBinding()] + param( + [switch]$SkipGitRemotes, + [switch]$Update + ) + + if (-not (Initialize-CursorRulesEnvironment -Update:$Update)) { + return + } + + Install-CursorInCurrentProject -SkipGitRemotes:$SkipGitRemotes + Write-Host '[i] Remotes policy: kalinamall=truth; home=Push-HomeMirror; github=Publish-GithubOrphan' -ForegroundColor DarkCyan + Write-Host '[+] init-cursor complete' -ForegroundColor Green +} + +if ($MyInvocation.InvocationName -eq '.') { + Write-Host '[+] init-cursor loaded - run: init-cursor' -ForegroundColor DarkCyan +} diff --git a/init-cursor.sh b/init-cursor.sh new file mode 100644 index 0000000..5627598 --- /dev/null +++ b/init-cursor.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash +# Инициализация Cursor в текущем проекте; на новом хосте сначала настраивает git/SSH. + +get_cursor_rules_dest() { + printf '%s' "$HOME/CursorRules" +} + +get_cursor_bootstrap_root() { + local rules_dest script_dir + rules_dest="$(get_cursor_rules_dest)" + script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + + if [[ -f "$script_dir/scripts/invoke-machine-bootstrap.sh" ]]; then + printf '%s' "$script_dir" + return 0 + fi + if [[ -f "$rules_dest/local/git-ssh-hosts.conf" ]]; then + printf '%s' "$rules_dest" + return 0 + fi + if [[ -f "$script_dir/local/git-ssh-hosts.conf" ]]; then + printf '%s' "$script_dir" + return 0 + fi + return 1 +} + +initialize_cursor_rules_environment() { + local update=0 + [[ "${1:-}" == '--update' ]] && update=1 + + local rules_dest bootstrap_root + rules_dest="$(get_cursor_rules_dest)" + bootstrap_root="$(get_cursor_bootstrap_root)" || { + echo 'Error: CursorRules not found. Run install.sh from Gitea first:' >&2 + echo ' curl -fsSL https://git.papatramp.ru/PapaTramp/Rules/raw/branch/main/install.sh | bash' >&2 + return 1 + } + + local skip_clone=0 + [[ -d "$rules_dest/.git" ]] && skip_clone=1 + + ROOT="$bootstrap_root" RULES_DEST="$rules_dest" SKIP_CLONE="$skip_clone" \ + bash "$bootstrap_root/scripts/invoke-machine-bootstrap.sh" + + if [[ "$update" -eq 1 && -d "$rules_dest/.git" ]]; then + echo '[*] Updating CursorRules from remote...' + if git -C "$rules_dest" pull --ff-only; then + echo '[+] CursorRules updated' + else + echo '[!] git pull failed in CursorRules' >&2 + fi + fi +} + +install_cursor_in_current_project() { + local skip_git_remotes=0 + while [[ $# -gt 0 ]]; do + case "$1" in + --skip-git-remotes) skip_git_remotes=1; shift ;; + *) shift ;; + esac + done + + local source rules_src scripts + source="$(get_cursor_rules_dest)" + rules_src="$source/.cursor/rules" + scripts="$source/scripts" + + 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' line + 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" -eq 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 + 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 + fi + fi +} + +init-cursor() { + 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 [[ "$update" -eq 1 ]]; then + initialize_cursor_rules_environment --update || return 1 + else + initialize_cursor_rules_environment || return 1 + fi + + if [[ "$skip_git_remotes" -eq 1 ]]; then + install_cursor_in_current_project --skip-git-remotes + else + install_cursor_in_current_project + fi + echo '[i] Remotes policy: kalinamall=truth; home=Push-HomeMirror; github=Publish-GithubOrphan' + echo '[+] init-cursor complete' +} diff --git a/install.ps1 b/install.ps1 new file mode 100644 index 0000000..773426f --- /dev/null +++ b/install.ps1 @@ -0,0 +1,65 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Точка входа на новом ПК: clone Rules, bootstrap, init-cursor в текущем проекте. + +.DESCRIPTION + SSH / HTTPS / ключи / remotes — автоматически. Запускать из каталога проекта. + На голом ПК: скопируйте этот файл с рабочей машины (scp/USB), затем: + & .\install.ps1 + +.EXAMPLE + cd C:\path\to\your-repo + & $env:TEMP\install.ps1 +#> +[CmdletBinding()] +param( + [switch]$SkipGitRemotes, + [switch]$Update +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$rulesDest = Join-Path $HOME 'CursorRules' +$httpsUrl = 'https://git.papatramp.ru/PapaTramp/Rules.git' +$sshUrl = 'ssh://git@git.papatramp.ru:2222/PapaTramp/Rules.git' + +function Ensure-RulesClone { + if (Test-Path (Join-Path $rulesDest '.git')) { + Write-Host '[*] Updating CursorRules...' -ForegroundColor Cyan + git -C $rulesDest pull --ff-only + return + } + + if (Test-Path -LiteralPath $rulesDest) { + $backup = "${rulesDest}.bak-$(Get-Date -Format 'yyyyMMdd-HHmmss')" + Write-Host "[!] Replacing non-git folder: $rulesDest -> $backup" -ForegroundColor Yellow + Move-Item -LiteralPath $rulesDest -Destination $backup + } + + Write-Host '[*] Cloning Rules via SSH...' -ForegroundColor Cyan + git clone --depth 1 $sshUrl $rulesDest 2>&1 | Out-Null + if ($LASTEXITCODE -ne 0) { + Write-Host '[!] SSH clone failed - trying HTTPS (enter Gitea login once)...' -ForegroundColor Yellow + git clone --depth 1 $httpsUrl $rulesDest + if ($LASTEXITCODE -ne 0) { + throw 'git clone failed' + } + } + Write-Host "[+] Cloned to $rulesDest" -ForegroundColor Green +} + +Ensure-RulesClone + +. (Join-Path $rulesDest 'init-cursor.ps1') + +if ($Update) { + init-cursor -Update -SkipGitRemotes:$SkipGitRemotes +} +elseif ($SkipGitRemotes) { + init-cursor -SkipGitRemotes +} +else { + init-cursor +} diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..acca195 --- /dev/null +++ b/install.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# Первая загрузка Rules и init-cursor. SSH/HTTPS и bootstrap — автоматически. +set -euo pipefail + +RULES_DEST="${RULES_DEST:-$HOME/CursorRules}" +HTTPS_URL="${HTTPS_URL:-https://git.papatramp.ru/PapaTramp/Rules.git}" +SSH_URL="${SSH_URL:-ssh://git@git.papatramp.ru:2222/PapaTramp/Rules.git}" +SKIP_GIT_REMOTES=0 +UPDATE=0 + +while [[ $# -gt 0 ]]; do + case "$1" in + --skip-git-remotes) SKIP_GIT_REMOTES=1; shift ;; + --update|-Update) UPDATE=1; shift ;; + *) shift ;; + esac +done + +log_step() { printf '[*] %s\n' "$1"; } +log_ok() { printf '[+] %s\n' "$1"; } +log_warn() { printf '[!] %s\n' "$1"; } + +ensure_rules_clone() { + if [[ -d "$RULES_DEST/.git" ]]; then + log_step 'Updating CursorRules...' + git -C "$RULES_DEST" pull --ff-only + return 0 + fi + + if [[ -e "$RULES_DEST" ]]; then + local backup="${RULES_DEST}.bak-$(date +%Y%m%d-%H%M%S)" + log_warn "Replacing non-git folder: $RULES_DEST -> $backup" + mv "$RULES_DEST" "$backup" + fi + + log_step 'Cloning Rules via SSH...' + if git clone --depth 1 "$SSH_URL" "$RULES_DEST" 2>/dev/null; then + log_ok "Cloned to $RULES_DEST" + return 0 + fi + + log_warn 'SSH clone failed - trying HTTPS (enter Gitea login once)...' + git clone --depth 1 "$HTTPS_URL" "$RULES_DEST" + log_ok "Cloned to $RULES_DEST" +} + +ensure_rules_clone + +# shellcheck source=/dev/null +source "$RULES_DEST/init-cursor.sh" + +if [[ "$UPDATE" -eq 1 ]]; then + if [[ "$SKIP_GIT_REMOTES" -eq 1 ]]; then + init-cursor --update --skip-git-remotes + else + init-cursor --update + fi +elif [[ "$SKIP_GIT_REMOTES" -eq 1 ]]; then + init-cursor --skip-git-remotes +else + init-cursor +fi diff --git a/local/git-ssh-hosts.conf b/local/git-ssh-hosts.conf new file mode 100644 index 0000000..4725bd3 --- /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=192.168.160.129 +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=192.168.128.48 +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..934f833 --- /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=R3tt3tnatur +KeyName=id_ed25519_git_kalinamall +KeyComment=papatramp@kalinamall diff --git a/local/papatramp-gitea.conf b/local/papatramp-gitea.conf new file mode 100644 index 0000000..cbec8ec --- /dev/null +++ b/local/papatramp-gitea.conf @@ -0,0 +1,11 @@ +Type=gitea +GiteaHost=git.papatramp.ru +GiteaPort=2222 +GiteaWebUrl=https://git.papatramp.ru +GiteaApiUrl=http://192.168.128.48:3000 +GiteaApiUser=papatramp +GiteaApiPassword=[eqyfhskj +HostAlias=git.papatramp.ru +KeyName=id_ed25519_git_papatramp +KeyComment=papatramp@papatramp.ru +ExtraHosts=git.papatramp.lan|192.168.128.48|2222,git.papatramp-haproxy|192.168.128.65|2222 diff --git a/scripts/Ensure-GiteaHomeRepo.ps1 b/scripts/Ensure-GiteaHomeRepo.ps1 new file mode 100644 index 0000000..939321e --- /dev/null +++ b/scripts/Ensure-GiteaHomeRepo.ps1 @@ -0,0 +1,63 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Создать репозиторий на home Gitea через API, если его ещё нет. +#> +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)][string]$RepoName, + [string]$Owner = 'PapaTramp', + [string]$ConfigPath = "$HOME\CursorRules\local\papatramp-gitea.conf", + [string]$Description = '' +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +if (-not (Test-Path -LiteralPath $ConfigPath)) { + throw "Config not found: $ConfigPath" +} + +$cfg = @{} +Get-Content -LiteralPath $ConfigPath | ForEach-Object { + if ($_ -match '^([^#=]+)=(.*)$') { + $cfg[$Matches[1].Trim()] = $Matches[2].Trim() + } +} + +$base = $cfg.GiteaApiUrl.TrimEnd('/') +$pair = '{0}:{1}' -f $cfg.GiteaApiUser, $cfg.GiteaApiPassword +$auth = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes($pair)) +$headers = @{ Authorization = $auth; 'Content-Type' = 'application/json' } + +$checkUrl = "$base/api/v1/repos/$Owner/$RepoName" +try { + $existing = Invoke-RestMethod -Method Get -Uri $checkUrl -Headers $headers -TimeoutSec 15 + Write-Host "[+] EXISTS $($existing.full_name)" -ForegroundColor Green + exit 0 +} +catch { + if ($_.Exception.Response.StatusCode.value__ -ne 404) { + throw "check failed: $($_.Exception.Message)" + } +} + +$body = (@{ + name = $RepoName + private = $true + auto_init = $false + description = $Description +} | ConvertTo-Json) + +foreach ($url in @("$base/api/v1/orgs/$Owner/repos", "$base/api/v1/user/repos")) { + try { + $created = Invoke-RestMethod -Method Post -Uri $url -Headers $headers -Body $body -TimeoutSec 30 + Write-Host "[+] CREATED $($created.full_name)" -ForegroundColor Green + exit 0 + } + catch { + Write-Warning "create via $url : $($_.Exception.Message)" + } +} + +throw 'failed to create repository on home Gitea' diff --git a/scripts/Invoke-MachineBootstrap.ps1 b/scripts/Invoke-MachineBootstrap.ps1 new file mode 100644 index 0000000..48c60c3 --- /dev/null +++ b/scripts/Invoke-MachineBootstrap.ps1 @@ -0,0 +1,210 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Настройка git на машине: user, SSH, Gitea keys, clone/update Rules. +#> +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)][string]$Root, + [string]$RulesDest = '', + [string]$CloneUrl = 'ssh://git@git.papatramp.ru:2222/PapaTramp/Rules.git', + [string]$HttpsCloneUrl = 'https://git.papatramp.ru/PapaTramp/Rules.git', + [switch]$SkipClone, + [switch]$SkipProfile, + [switch]$SkipGitSetup, + [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 Test-MachineGitReady { + param([string]$ConfigDir) + + $hostsConfig = Join-Path $ConfigDir 'git-ssh-hosts.conf' + if (-not (Test-Path -LiteralPath $hostsConfig)) { return $false } + + $sshConfig = Join-Path $HOME '.ssh\config' + if (-not (Test-Path -LiteralPath $sshConfig)) { return $false } + + $profiles = @{} + $currentProfile = $null + Get-Content -LiteralPath $hostsConfig | ForEach-Object { + $line = $_.Trim() + 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() + } + } + + $sshText = Get-Content -LiteralPath $sshConfig -Raw + foreach ($profile in $profiles.Values) { + $keyName = $profile.KeyName + if (-not $keyName) { continue } + $keyPath = Join-Path $HOME ".ssh\$keyName" + if (-not (Test-Path -LiteralPath $keyPath)) { return $false } + } + + if ($sshText -notmatch '(?m)^Host\s+git\.kalinamall\.ru\s*$') { return $false } + return $true +} + +function Ensure-RulesRepository { + param( + [string]$Destination, + [string]$SshUrl, + [string]$HttpsUrl + ) + + if (Test-Path -LiteralPath $Destination) { + if (Test-Path (Join-Path $Destination '.git')) { + if ($WhatIf) { + Write-Step "WhatIf: git pull $Destination" + return + } + Write-Step "Updating $Destination" + git -C $Destination pull --ff-only + Write-Ok 'CursorRules updated' + return + } + + $backup = "${Destination}.bak-$(Get-Date -Format 'yyyyMMdd-HHmmss')" + Write-Warn "Replacing non-git folder: $Destination -> $backup" + if (-not $WhatIf) { + Move-Item -LiteralPath $Destination -Destination $backup + } + } + + if ($WhatIf) { + Write-Step "WhatIf: clone -> $Destination" + return + } + + Write-Step "Cloning Rules via SSH: $SshUrl" + git clone --depth 1 $SshUrl $Destination 2>&1 | Out-Null + if ($LASTEXITCODE -eq 0) { + Write-Ok "Cloned to $Destination" + return + } + + Write-Warn 'SSH clone failed - trying HTTPS (enter Gitea login once)' + git clone --depth 1 $HttpsUrl $Destination + if ($LASTEXITCODE -ne 0) { + throw "Failed to clone Rules repository" + } + Write-Ok "Cloned via HTTPS to $Destination" +} + +function Ensure-InitCursorProfile { + param([string]$RulesDest) + + $profileHook = ". `"$RulesDest\init-cursor.ps1`"" + $profilePath = $PROFILE.CurrentUserAllHosts + if (-not $profilePath) { $profilePath = $PROFILE.CurrentUserCurrentHost } + + if ($WhatIf) { + Write-Step "WhatIf: profile hook -> $profilePath" + return + } + + $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" + } +} + +if (-not $RulesDest) { + $RulesDest = Join-Path $HOME 'CursorRules' +} + +$configDir = Join-Path $Root 'local' +if (-not (Test-Path -LiteralPath $configDir)) { + throw "Bootstrap config not found: $configDir" +} + +$gitReady = Test-MachineGitReady -ConfigDir $configDir +$repoReady = Test-Path (Join-Path $RulesDest '.git') + +if (-not $gitReady -and -not $SkipGitSetup) { + Write-Step 'Machine bootstrap: git + SSH' + +$userCfgPath = Join-Path $configDir 'git-user.conf' +if (Test-Path -LiteralPath $userCfgPath) { + $userCfg = Read-KeyValueFile -Path $userCfgPath + if ($userCfg.UserName -and -not $WhatIf) { + git config --global user.name $userCfg.UserName | Out-Null + Write-Ok "git user.name = $($userCfg.UserName)" + } + if ($userCfg.UserEmail -and -not $WhatIf) { + 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.ru' + ssh -T -o BatchMode=yes -o ConnectTimeout=10 git@git.kalinamall.ru 2>&1 | ForEach-Object { Write-Host $_ } +} +} +elseif ($gitReady) { + Write-Ok 'Git SSH already configured' +} + +if (-not $SkipClone -and -not $repoReady) { + Ensure-RulesRepository -Destination $RulesDest -SshUrl $CloneUrl -HttpsUrl $HttpsCloneUrl +} +elseif ($repoReady -and -not $SkipClone -and -not $WhatIf) { + Write-Ok "CursorRules repo OK: $RulesDest" +} + +if (-not $SkipProfile) { + Ensure-InitCursorProfile -RulesDest $RulesDest +} + +Write-Ok 'Machine bootstrap complete' + +# Export test function for init-cursor dot-source +function Test-CursorMachineGitReady { + param([string]$ConfigDir = (Join-Path $HOME 'CursorRules\local')) + return Test-MachineGitReady -ConfigDir $ConfigDir +} diff --git a/scripts/Publish-GithubOrphan.ps1 b/scripts/Publish-GithubOrphan.ps1 new file mode 100644 index 0000000..36d3760 --- /dev/null +++ b/scripts/Publish-GithubOrphan.ps1 @@ -0,0 +1,144 @@ +#Requires -Version 5.1 +# Publish kalinamall/main tree to github.com/PTah as a single orphan commit (no history). +# Hard sanitize: internal URLs/hosts, site names, Cursor trailers/mentions. +param( + [Parameter(Mandatory = $false)] + [string]$SourceRepo = (Get-Location).Path, + [string]$RemoteName = 'github', + [string]$Version = '', + [string[]]$ExcludePaths = @(), + [hashtable]$OverlayFromGithub = @{} +) + +$ErrorActionPreference = 'Stop' +$SourceRepo = (Resolve-Path $SourceRepo).Path +Set-Location $SourceRepo + +$remoteUrl = (git remote get-url $RemoteName).Trim() +if (-not $remoteUrl) { throw "remote not found: $RemoteName" } + +$work = Join-Path $env:TEMP "gh-orphan-$([IO.Path]::GetFileName($SourceRepo))-$PID" +if (Test-Path $work) { Remove-Item $work -Recurse -Force } +New-Item -ItemType Directory -Path $work | Out-Null + +$zip = Join-Path $env:TEMP "gh-archive-$PID.zip" +if (Test-Path $zip) { Remove-Item $zip -Force } +git archive --format=zip -o $zip main +Expand-Archive -Path $zip -DestinationPath $work -Force +Remove-Item $zip -Force + +foreach ($drop in @('.cursor', '.cursorignore', 'AGENTS.md', 'CURSOR.md')) { + $t = Join-Path $work $drop + if (Test-Path $t) { + Remove-Item $t -Recurse -Force + Write-Host "exclude: $drop" + } +} + +foreach ($rel in $ExcludePaths) { + $target = Join-Path $work $rel + if (Test-Path $target) { + Remove-Item $target -Recurse -Force + Write-Host "exclude: $rel" + } +} + +foreach ($entry in $OverlayFromGithub.GetEnumerator()) { + $rel = $entry.Key + $content = git show "${RemoteName}/main:$rel" 2>$null + if ($LASTEXITCODE -ne 0) { throw "overlay missing on ${RemoteName}/main: $rel" } + $out = Join-Path $work $rel + $dir = Split-Path $out -Parent + if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } + [IO.File]::WriteAllText($out, ($content -join "`n") + "`n", [Text.UTF8Encoding]::new($false)) + Write-Host "overlay: $rel" +} + +$textExt = @('.md', '.py', '.ts', '.vue', '.json', '.sh', '.ps1', '.yml', '.yaml', '.example', '.service', '.txt', '.html', '.css', '.kt', '.kts', '.xml', '.properties', '.gradle', '.pro') +$replacements = @( + @{ From = 'https://git\.kalinamall\.ru/PapaTramp/([^)/\s"''`]+)/src/branch/main/'; To = 'https://git.papatramp.ru/PapaTramp/$1/src/branch/main/' } + @{ From = 'https://git\.kalinamall\.ru/PapaTramp/([^)/\s"''`]+)/blob/main/'; To = 'https://git.papatramp.ru/PapaTramp/$1/src/branch/main/' } + @{ From = 'https://git\.kalinamall\.ru/PapaTramp/'; To = 'https://git.papatramp.ru/PapaTramp/' } + @{ From = 'git\.kalinamall\.ru/PapaTramp/'; To = 'git.papatramp.ru/PapaTramp/' } + @{ From = 'https://git\.papatramp\.ru/(PapaTramp|PTah)/'; To = 'https://git.papatramp.ru/PapaTramp/' } + @{ From = 'git\.papatramp\.ru/(PapaTramp|PTah)/'; To = 'git.papatramp.ru/PapaTramp/' } + @{ From = 'ssh://git@git\.kalinamall\.ru:2222/PapaTramp/'; To = 'https://git.papatramp.ru/PapaTramp/' } + @{ From = 'ssh://git@git\.papatramp\.ru:2222/PapaTramp/'; To = 'https://git.papatramp.ru/PapaTramp/' } + @{ From = 'sac-api\.kalinamall\.ru'; To = 'sac-api.example.com' } + @{ From = 'sac\.kalinamall\.ru'; To = 'sac.example.com' } + @{ From = 'ext\.kalinamall\.ru'; To = 'ext.example.com' } + @{ From = '\*\.kalinamall\.ru'; To = '*.example.com' } + @{ From = 'kalinamall\.ru'; To = 'example.com' } + @{ From = 'papatramp\.lan'; To = 'example.lan' } + @{ From = '192\.168\.\d+\.\d+'; To = '10.0.0.1' } + @{ From = 'Co-authored-by:\s*Cursor\s*\s*'; To = '' } + @{ From = 'Made-with:\s*Cursor\s*'; To = '' } + @{ From = 'Cursor Agent'; To = '' } + @{ From = 'Cursor IDE'; To = '' } + @{ From = 'cursoragent@cursor\.com'; To = '' } + @{ From = '(?i)\bpowered by cursor\b'; To = '' } + @{ From = 'B26\\'; To = 'CONTOSO\' } + @{ From = 'B26/'; To = 'CONTOSO/' } + @{ From = 'K6A-DC\d'; To = 'WIN-DC01' } + @{ From = 'UNMS Kalina'; To = 'Example Site' } + @{ From = 'k\.khodasevich'; To = 'user1' } + @{ From = '\bkalinamall\b'; To = 'example' } + @{ From = '\bpapatramp\b'; To = 'jdoe' } +) + +Get-ChildItem -Path $work -Recurse -File | ForEach-Object { + $ext = $_.Extension.ToLowerInvariant() + if ($textExt -notcontains $ext -and $_.Name -notmatch '\.(example|service)$') { return } + $content = [IO.File]::ReadAllText($_.FullName) + $updated = $content + foreach ($r in $replacements) { + $updated = [regex]::Replace($updated, $r.From, $r.To) + } + if ($updated -ne $content) { + [IO.File]::WriteAllText($_.FullName, $updated, [Text.UTF8Encoding]::new($false)) + } +} + +$forbidden = @( + 'git\.kalinamall\.ru', + 'git\.papatramp\.ru', + 'papatramp\.lan', + '192\.168\.\d+\.\d+', + 'cursoragent@cursor\.com', + 'Co-authored-by:\s*Cursor', + 'Made-with:\s*Cursor', + '(?i)Cursor Agent', + '(?i)Cursor IDE', + '(?i)powered by cursor', + '\bkalinamall\b', + '\bpapatramp\b', + 'K6A-DC', + 'B26\\' +) +$hits = 0 +Get-ChildItem -Path $work -Recurse -File | ForEach-Object { + $ext = $_.Extension.ToLowerInvariant() + if ($textExt -notcontains $ext -and $_.Name -notmatch '\.(example|service)$') { return } + $content = [IO.File]::ReadAllText($_.FullName) + foreach ($pat in $forbidden) { + if ([regex]::IsMatch($content, $pat)) { + $rel = $_.FullName.Substring($work.Length + 1) + Write-Warning "FORBIDDEN '$pat' in $rel" + $script:hits++ + } + } +} +if ($hits -gt 0) { throw "sanitization failed: $hits forbidden pattern(s)" } + +Set-Location $work +if (Test-Path .git) { Remove-Item .git -Recurse -Force } +git init -b main | Out-Null +git add -A +$msg = if ($Version) { "chore(github): sync public mirror ($Version)" } else { 'chore(github): sync public mirror' } +git -c user.name='PTah' -c user.email='papatramp@gmail.com' commit -m $msg +git remote add target $remoteUrl +git push target main --force +Write-Host "OK -> $remoteUrl (orphan, force)" + +Set-Location $SourceRepo +Remove-Item $work -Recurse -Force \ No newline at end of file diff --git a/scripts/Push-HomeMirror.ps1 b/scripts/Push-HomeMirror.ps1 new file mode 100644 index 0000000..6c3981d --- /dev/null +++ b/scripts/Push-HomeMirror.ps1 @@ -0,0 +1,60 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Mirror current kalinamall/main tree to remote "home" with papatramp URLs (force). +.DESCRIPTION + Canonical working tree stays on kalinamall. This builds a temp orphan commit + with git host URLs rewritten to git.papatramp.ru and force-pushes to home. + Private content (LAN, names, secrets) is kept — unlike Publish-GithubOrphan. +#> +param( + [string]$SourceRepo = (Get-Location).Path, + [string]$Branch = 'main', + [string]$RemoteName = 'home' +) + +$ErrorActionPreference = 'Stop' +$SourceRepo = (Resolve-Path $SourceRepo).Path +Set-Location $SourceRepo + +$rewrite = Join-Path $PSScriptRoot 'Rewrite-GitHostUrls.ps1' +if (-not (Test-Path $rewrite)) { throw "missing: $rewrite" } + +$ensure = Join-Path $PSScriptRoot 'Ensure-GiteaHomeRepo.ps1' +$repoName = Split-Path -Leaf (git rev-parse --show-toplevel) +if (Test-Path $ensure) { + try { + & powershell -NoProfile -ExecutionPolicy Bypass -File $ensure -RepoName $repoName + } catch { + Write-Warning "Ensure-GiteaHomeRepo: $($_.Exception.Message)" + } +} + +$remoteUrl = (git remote get-url $RemoteName).Trim() +if (-not $remoteUrl) { throw "remote not found: $RemoteName" } + +$work = Join-Path $env:TEMP "home-mirror-$repoName-$PID" +if (Test-Path $work) { Remove-Item $work -Recurse -Force } +New-Item -ItemType Directory -Path $work | Out-Null + +$zip = Join-Path $env:TEMP "home-archive-$PID.zip" +if (Test-Path $zip) { Remove-Item $zip -Force } +git archive --format=zip -o $zip $Branch +Expand-Archive -Path $zip -DestinationPath $work -Force +Remove-Item $zip -Force + +& powershell -NoProfile -ExecutionPolicy Bypass -File $rewrite -Target home -Root $work + +Set-Location $work +if (Test-Path .git) { Remove-Item .git -Recurse -Force } +git init -b main | Out-Null +git add -A +$sha = (git -C $SourceRepo rev-parse --short HEAD).Trim() +$msg = "chore(home): mirror from kalinamall ($sha) with papatramp URLs" +git -c user.name='PTah' -c user.email='papatramp@gmail.com' commit -m $msg | Out-Null +git remote add target $remoteUrl +git push target main --force +Write-Host "OK -> $remoteUrl (home mirror, force)" + +Set-Location $SourceRepo +Remove-Item $work -Recurse -Force 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/Rewrite-GitHostUrls.ps1 b/scripts/Rewrite-GitHostUrls.ps1 new file mode 100644 index 0000000..fc75f82 --- /dev/null +++ b/scripts/Rewrite-GitHostUrls.ps1 @@ -0,0 +1,91 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Rewrite cross-repo git host URLs in tracked text files for the target remote. +.PARAMETER Target + kalinamall | home | papatramp | github +#> +param( + [Parameter(Mandatory = $true)] + [ValidateSet('github', 'kalinamall', 'home', 'papatramp')] + [string]$Target, + + [string]$Root = (Get-Location).Path +) + +$ErrorActionPreference = 'Stop' +$Root = (Resolve-Path $Root).Path +Set-Location $Root + +if ($Target -eq 'home') { $Target = 'papatramp' } + +switch ($Target) { + 'github' { + $Base = 'https://github.com/PTah' + $BlobSuffix = '/blob/main' + } + 'kalinamall' { + $Base = 'https://git.kalinamall.ru/PapaTramp' + $BlobSuffix = '/src/branch/main' + } + 'papatramp' { + $Base = 'https://git.papatramp.ru/PapaTramp' + $BlobSuffix = '/src/branch/main' + } +} + +$BaseHost = $Base -replace '^https://', '' +$sshTo = switch ($Target) { + 'github' { 'https://git.papatramp.ru/PapaTramp/' } + 'kalinamall' { 'ssh://git@git.papatramp.ru:2222/PapaTramp/' } + default { 'ssh://git@git.papatramp.ru:2222/PapaTramp/' } +} + +$patterns = @( + @{ From = 'https://github\.com/PTah/([^)/''"\s]+)/blob/main/'; To = "$Base/`${1}$BlobSuffix/" } + @{ From = 'https://github\.com/PTah/([^)/''"\s]+)/src/branch/main/'; To = "$Base/`${1}$BlobSuffix/" } + @{ From = 'https://git\.kalinamall\.ru/PapaTramp/([^)/''"\s]+)/blob/main/'; To = "$Base/`${1}$BlobSuffix/" } + @{ From = 'https://git\.kalinamall\.ru/PapaTramp/([^)/''"\s]+)/src/branch/main/'; To = "$Base/`${1}$BlobSuffix/" } + @{ From = 'https://git\.papatramp\.ru/PapaTramp/([^)/''"\s]+)/src/branch/main/'; To = "$Base/`${1}$BlobSuffix/" } + @{ From = 'https://git\.papatramp\.ru/PTah/([^)/''"\s]+)/src/branch/main/'; To = "$Base/`${1}$BlobSuffix/" } + @{ From = 'https://github\.com/PTah/'; To = "$Base/" } + @{ From = 'https://git\.kalinamall\.ru/PapaTramp/'; To = "$Base/" } + @{ From = 'https://git\.papatramp\.ru/PapaTramp/'; To = "$Base/" } + @{ From = 'https://git\.papatramp\.ru/PTah/'; To = "$Base/" } + @{ From = 'ssh://git@git\.kalinamall\.ru:2222/PapaTramp/'; To = $sshTo } + @{ From = 'ssh://git@git\.papatramp\.ru:2222/PapaTramp/'; To = $sshTo } + @{ From = 'github\.com/PTah/'; To = "$BaseHost/" } + @{ From = 'git\.kalinamall\.ru/PapaTramp/'; To = "$BaseHost/" } + @{ From = 'git\.papatramp\.ru/PapaTramp/'; To = "$BaseHost/" } + @{ From = 'git\.papatramp\.ru/PTah/'; To = "$BaseHost/" } +) + +$extensions = @('*.md', '*.json', '*.service', '*.example', '*.sh', '*.ps1', '*.yml', '*.yaml', '*.txt', '*.kt', '*.kts', '*.xml', '*.html', '*.css', '*.ts', '*.vue', '*.py') +$files = @() +if (Test-Path (Join-Path $Root '.git')) { + $files = @(git -C $Root ls-files $extensions 2>$null | Where-Object { $_ -and (Test-Path (Join-Path $Root $_)) }) +} +if ($files.Count -eq 0) { + $files = @(Get-ChildItem -Path $Root -Recurse -File -Include $extensions | + Where-Object { $_.FullName -notmatch '\\(\.git|node_modules|\.venv|venv|dist|build)\\' } | + ForEach-Object { $_.FullName.Substring($Root.Length).TrimStart('\', '/') }) +} + +$n = 0 +foreach ($file in $files) { + $path = Join-Path $Root $file + $content = [System.IO.File]::ReadAllText($path) + $updated = $content + foreach ($p in $patterns) { + $updated = [regex]::Replace($updated, $p.From, $p.To) + } + if ($updated -ne $content) { + # UTF-8 BOM for .ps1 (Windows PS 5.1); otherwise UTF-8 no BOM + $bom = ($path -match '\.(ps1|psm1)$') + [System.IO.File]::WriteAllText($path, $updated, [System.Text.UTF8Encoding]::new($bom)) + Write-Output "updated: $file" + $n++ + } +} + +Write-Output "Rewrite-GitHostUrls: target=$Target base=$Base files_changed=$n" diff --git a/scripts/Set-GitRemotes.ps1 b/scripts/Set-GitRemotes.ps1 new file mode 100644 index 0000000..c166009 --- /dev/null +++ b/scripts/Set-GitRemotes.ps1 @@ -0,0 +1,132 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Стандартные git remote: home (papatramp), kalinamall, github (PTah). + +.EXAMPLE + cd C:\Users\papat\Projects\MyRepo + & "$HOME\CursorRules\scripts\Set-GitRemotes.ps1" +#> +[CmdletBinding()] +param( + [string]$Owner = 'PapaTramp', + [string]$GithubOrg = 'PTah', + [string]$Repo = '', + [switch]$SkipKalinamall, + [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 Test-GitRepo { + git rev-parse --is-inside-work-tree 2>$null | Out-Null + if ($LASTEXITCODE -ne 0) { + throw 'Run inside a git repository or pass -Repo.' + } +} + +function Get-RepoNameFromRemoteUrl { + param([string]$Url) + if ($Url -match '/([^/]+?)(?:\.git)?/?$') { + return $Matches[1] + } + return $null +} + +function Get-RepoName { + param([string]$Name) + if ($Name) { return $Name } + + foreach ($remote in @('kalinamall', 'home', 'github', 'origin')) { + $url = git remote get-url $remote 2>$null + if ($url) { + $fromUrl = Get-RepoNameFromRemoteUrl -Url $url.Trim() + if ($fromUrl) { return $fromUrl } + } + } + + return (Split-Path -Leaf (git rev-parse --show-toplevel)) +} + +function Get-OriginRemoteTarget { + $url = (git remote get-url origin 2>$null) + if (-not $url) { return $null } + $lower = $url.ToLower() + if ($lower -match 'kalinamall') { return 'kalinamall' } + if ($lower -match 'papatramp') { return 'home' } + if ($lower -match 'github\.com') { return 'github' } + return $null +} + +function Set-Remote { + param( + [string]$Name, + [string]$Url + ) + + $existing = @(git remote 2>$null) + $has = $existing -contains $Name + + if ($WhatIf) { + Write-Step "WhatIf: remote $Name -> $Url" + return + } + + if ($has) { + $current = (git remote get-url $Name 2>$null).Trim() + if ($current -eq $Url) { + Write-Ok "remote $Name OK" + return + } + git remote set-url $Name $Url + Write-Ok "remote $Name updated -> $Url" + return + } + + if ($existing -contains 'origin') { + $originTarget = Get-OriginRemoteTarget + if ($originTarget -eq $Name) { + git remote rename origin $Name + git remote set-url $Name $Url + Write-Ok "remote origin renamed to $Name -> $Url" + return + } + } + + $legacy = @{ + home = @('gitea', 'papatramp', 'origin-home') + github = @() + kalinamall = @('gitea-kalinamall', 'kalina') + } + foreach ($old in $legacy[$Name]) { + if ($existing -contains $old) { + git remote rename $old $Name + git remote set-url $Name $Url + Write-Ok "remote $old renamed to $Name -> $Url" + return + } + } + + git remote add $Name $Url + Write-Ok "remote $Name added -> $Url" +} + +Test-GitRepo +$repoName = Get-RepoName -Name $Repo + +$homeUrl = "ssh://git@git.papatramp.ru:2222/${Owner}/${repoName}.git" +$kalinamallUrl = "ssh://git@git.kalinamall.ru:2222/${Owner}/${repoName}.git" +$githubUrl = "git@github.com:${GithubOrg}/${repoName}.git" + +Write-Step "Repo: $repoName" +Set-Remote -Name 'home' -Url $homeUrl +Set-Remote -Name 'kalinamall' -Url $kalinamallUrl +Set-Remote -Name 'github' -Url $githubUrl + +Write-Host '' +Write-Host 'Remotes:' -ForegroundColor Magenta +git remote -v diff --git a/scripts/Set-GitRemotes.sh b/scripts/Set-GitRemotes.sh new file mode 100644 index 0000000..a01c874 --- /dev/null +++ b/scripts/Set-GitRemotes.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash +# Стандартные git remote: home (papatramp), kalinamall, github (PTah). +set -euo pipefail + +OWNER="${OWNER:-PapaTramp}" +GITHUB_ORG="${GITHUB_ORG:-PTah}" +REPO="${REPO:-}" +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_from_url() { + local url="$1" name="" + if [[ "$url" =~ /([^/]+)/?$ ]]; then + name="${BASH_REMATCH[1]}" + name="${name%.git}" + printf '%s\n' "$name" + fi +} + +repo_name="${REPO:-}" +if [[ -z "$repo_name" ]]; then + for remote in kalinamall home github origin; do + if url="$(git remote get-url "$remote" 2>/dev/null)"; then + repo_name="$(repo_name_from_url "$url")" + [[ -n "$repo_name" ]] && break + fi + done +fi +repo_name="${repo_name:-$(basename "$(git rev-parse --show-toplevel)")}" +repo_name="${repo_name%.git}" +home_url="ssh://git@git.papatramp.ru:2222/${OWNER}/${repo_name}.git" +kalinamall_url="ssh://git@git.kalinamall.ru:2222/${OWNER}/${repo_name}.git" +github_url="git@github.com:${GITHUB_ORG}/${repo_name}.git" + +origin_remote_target() { + local url + url="$(git remote get-url origin 2>/dev/null | tr '[:upper:]' '[:lower:]' || true)" + [[ -z "$url" ]] && return 0 + if [[ "$url" == *kalinamall* ]]; then echo kalinamall; return 0; fi + if [[ "$url" == *papatramp* ]]; then echo home; return 0; fi + if [[ "$url" == *github.com* ]]; then echo github; return 0; fi +} + +set_remote() { + local name="$1" + local url="$2" + local existing current old origin_target + + 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 + + if git remote | grep -Fxq origin; then + origin_target="$(origin_remote_target || true)" + if [[ "$origin_target" == "$name" ]]; then + git remote rename origin "$name" + git remote set-url "$name" "$url" + log_ok "remote origin renamed to $name -> $url" + return 0 + fi + fi + + case "$name" in + home) legacy=(gitea papatramp origin-home) ;; + github) legacy=() ;; + kalinamall) legacy=(gitea-kalinamall kalina) ;; + *) legacy=() ;; + esac + + if ((${#legacy[@]} > 0)); then + 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 + fi + + git remote add "$name" "$url" + log_ok "remote $name added -> $url" +} + +log_step "Repo: $repo_name" +set_remote home "$home_url" +set_remote kalinamall "$kalinamall_url" +set_remote github "$github_url" + +printf '\n' +git remote -v diff --git a/scripts/Setup-GitSsh.ps1 b/scripts/Setup-GitSsh.ps1 new file mode 100644 index 0000000..914361e --- /dev/null +++ b/scripts/Setup-GitSsh.ps1 @@ -0,0 +1,192 @@ +#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-SshConfigText { + param( + [string]$ExistingText, + [string]$HostAlias, + [string]$Block + ) + + $begin = "# BEGIN Setup-SshAccess:$HostAlias" + $end = "# END Setup-SshAccess:$HostAlias" + $existing = $ExistingText + + $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-SshConfigText -ExistingText $configText -HostAlias $hostAlias -Block $block + Write-Ok "ssh config: $hostAlias" +} + +if (-not $WhatIf) { + $utf8NoBom = New-Object System.Text.UTF8Encoding $false + [System.IO.File]::WriteAllText($sshConfigPath, $configText.TrimEnd() + "`n", $utf8NoBom) + + Get-Service ssh-agent -ErrorAction SilentlyContinue | Where-Object { $_.Status -ne 'Running' } | + ForEach-Object { Start-Service ssh-agent -ErrorAction SilentlyContinue | Out-Null } + + foreach ($keyName in $profileKeys.Keys) { + $keyPath = Join-Path $sshDir $keyName + if (Test-Path -LiteralPath $keyPath) { + try { ssh-add $keyPath 2>$null | Out-Null } catch { } + } + } +} + +Write-Ok 'SSH setup complete' diff --git a/scripts/invoke-machine-bootstrap.sh b/scripts/invoke-machine-bootstrap.sh new file mode 100644 index 0000000..918719b --- /dev/null +++ b/scripts/invoke-machine-bootstrap.sh @@ -0,0 +1,128 @@ +#!/usr/bin/env bash +# Настройка git на машине: user, SSH, Gitea keys, clone/update Rules. +set -euo pipefail + +ROOT="${ROOT:?ROOT is required}" +RULES_DEST="${RULES_DEST:-$HOME/CursorRules}" +CLONE_URL="${CLONE_URL:-ssh://git@git.papatramp.ru:2222/PapaTramp/Rules.git}" +HTTPS_CLONE_URL="${HTTPS_CLONE_URL:-https://git.papatramp.ru/PapaTramp/Rules.git}" +SKIP_CLONE="${SKIP_CLONE:-0}" +SKIP_PROFILE="${SKIP_PROFILE:-0}" +SKIP_GIT_SETUP="${SKIP_GIT_SETUP:-0}" +WHAT_IF="${WHAT_IF:-0}" + +log_step() { printf '[*] %s\n' "$1"; } +log_ok() { printf '[+] %s\n' "$1"; } +log_warn() { printf '[!] %s\n' "$1"; } + +CONFIG_DIR="$ROOT/local" +[[ -d "$CONFIG_DIR" ]] || { echo "Bootstrap config not found: $CONFIG_DIR" >&2; exit 1; } + +test_machine_git_ready() { + local hosts_config="$CONFIG_DIR/git-ssh-hosts.conf" + local ssh_config="$HOME/.ssh/config" + [[ -f "$hosts_config" && -f "$ssh_config" ]] || return 1 + grep -q '^Host git.kalinamall.ru$' "$ssh_config" || return 1 + + local line current_profile='' key_name + while IFS= read -r line || [[ -n "$line" ]]; do + line="${line%%#*}" + line="${line%"${line##*[![:space:]]}"}" + [[ -z "$line" ]] && continue + if [[ "$line" =~ ^\[profile:(.+)\]$ ]]; then + current_profile="${BASH_REMATCH[1]}" + continue + fi + if [[ "$line" =~ ^\[host: ]]; then + current_profile='' + continue + fi + if [[ "$line" == KeyName=* && -n "$current_profile" ]]; then + key_name="${line#KeyName=}" + [[ -f "$HOME/.ssh/$key_name" ]] || return 1 + fi + done < "$hosts_config" + return 0 +} + +ensure_rules_repository() { + if [[ -d "$RULES_DEST/.git" ]]; then + return 0 + fi + + if [[ -e "$RULES_DEST" ]]; then + local backup="${RULES_DEST}.bak-$(date +%Y%m%d-%H%M%S)" + log_warn "Replacing non-git folder: $RULES_DEST -> $backup" + if [[ "$WHAT_IF" != '1' ]]; then + mv "$RULES_DEST" "$backup" + fi + fi + + log_step "Cloning Rules via SSH: $CLONE_URL" + if [[ "$WHAT_IF" == '1' ]]; then + log_step "WhatIf: clone -> $RULES_DEST" + return 0 + fi + if git clone --depth 1 "$CLONE_URL" "$RULES_DEST" 2>/dev/null; then + log_ok "Cloned to $RULES_DEST" + return 0 + fi + + log_warn 'SSH clone failed - trying HTTPS (enter Gitea login once)' + git clone --depth 1 "$HTTPS_CLONE_URL" "$RULES_DEST" + log_ok "Cloned via HTTPS to $RULES_DEST" +} + +ensure_init_cursor_profile() { + local hook="source \"$RULES_DEST/init-cursor.sh\"" + local profile + for profile in "$HOME/.bashrc" "$HOME/.zshrc"; do + [[ -f "$profile" ]] || continue + if [[ "$WHAT_IF" == '1' ]]; then + log_step "WhatIf: profile hook -> $profile" + continue + fi + if ! grep -Fq "$hook" "$profile" 2>/dev/null; then + printf '\n%s\n' "$hook" >> "$profile" + log_ok "Profile updated: $profile" + fi + done +} + +git_ready=0 +repo_ready=0 +test_machine_git_ready && git_ready=1 +[[ -d "$RULES_DEST/.git" ]] && repo_ready=1 + +if [[ "$git_ready" -eq 0 && "$SKIP_GIT_SETUP" != '1' ]]; then + log_step 'Machine bootstrap: git + SSH' + if [[ -f "$CONFIG_DIR/git-user.conf" ]]; 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 < <(grep -v '^#' "$CONFIG_DIR/git-user.conf" | grep '=' || true) + fi + export CONFIG_DIR WHAT_IF + bash "$ROOT/scripts/setup-git-ssh.sh" + bash "$ROOT/scripts/register-gitea-ssh-keys.sh" + log_step 'Testing git@git.kalinamall.ru' + ssh -T -o BatchMode=yes -o ConnectTimeout=10 git@git.kalinamall.ru || true +elif [[ "$git_ready" -eq 1 ]]; then + log_ok 'Git SSH already configured' +fi + +if [[ "$SKIP_CLONE" != '1' && "$repo_ready" -eq 0 ]]; then + ensure_rules_repository +elif [[ "$repo_ready" -eq 1 ]]; then + log_ok "CursorRules repo OK: $RULES_DEST" +fi + +if [[ "$SKIP_PROFILE" != '1' ]]; then + ensure_init_cursor_profile +fi + +log_ok 'Machine bootstrap complete' diff --git a/scripts/publish-to-github.sh b/scripts/publish-to-github.sh new file mode 100644 index 0000000..c74d9b5 --- /dev/null +++ b/scripts/publish-to-github.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash +# Publish current main tree to github as orphan + hard sanitize (force). +# Usage: publish-to-github.sh [source_repo] [version] +set -euo pipefail + +SOURCE_REPO="${1:-$(pwd)}" +VERSION="${2:-}" +REMOTE_NAME="${REMOTE_NAME:-github}" +SOURCE_REPO="$(cd "$SOURCE_REPO" && pwd)" +cd "$SOURCE_REPO" + +REMOTE_URL="$(git remote get-url "$REMOTE_NAME")" +REPO_NAME="$(basename "$(git rev-parse --show-toplevel)")" +WORK="${TMPDIR:-/tmp}/gh-orphan-${REPO_NAME}-$$" + +rm -rf "$WORK" +mkdir -p "$WORK" +git archive --format=tar main | tar -x -C "$WORK" +rm -rf "$WORK/.cursor" "$WORK/.cursorignore" 2>/dev/null || true + +# URL + PII + Cursor sanitize +export LC_ALL=C +while IFS= read -r -d '' f; do + case "$f" in + *.md|*.py|*.ts|*.vue|*.json|*.sh|*.ps1|*.yml|*.yaml|*.example|*.service|*.txt|*.html|*.css|*.kt|*.kts|*.xml|*.properties|*.gradle|*.pro) ;; + *) continue ;; + esac + perl -i -pe ' + s#https://git\.kalinamall\.ru/PapaTramp/([^)/\s\"\x27`]+)/src/branch/main/#https://git.papatramp.ru/PapaTramp/$1/src/branch/main/#g; + s#https://git\.kalinamall\.ru/PapaTramp/([^)/\s\"\x27`]+)/blob/main/#https://git.papatramp.ru/PapaTramp/$1/src/branch/main/#g; + s#https://git\.kalinamall\.ru/PapaTramp/#https://git.papatramp.ru/PapaTramp/#g; + s#git\.kalinamall\.ru/PapaTramp/#git.papatramp.ru/PapaTramp/#g; + s#https://git\.papatramp\.ru/(PapaTramp|PTah)/#https://git.papatramp.ru/PapaTramp/#g; + s#git\.papatramp\.ru/(PapaTramp|PTah)/#git.papatramp.ru/PapaTramp/#g; + s#ssh://git\@git\.kalinamall\.ru:2222/PapaTramp/#https://git.papatramp.ru/PapaTramp/#g; + s#ssh://git\@git\.papatramp\.ru:2222/PapaTramp/#https://git.papatramp.ru/PapaTramp/#g; + s#sac-api\.kalinamall\.ru#sac-api.example.com#g; + s#sac\.kalinamall\.ru#sac.example.com#g; + s#ext\.kalinamall\.ru#ext.example.com#g; + s#\*\.kalinamall\.ru#*.example.com#g; + s#kalinamall\.ru#example.com#g; + s#papatramp\.lan#example.lan#g; + s#192\.168\.\d+\.\d+#10.0.0.1#g; + s#Co-authored-by:\s*Cursor\s*\s*##g; + s#Made-with:\s*Cursor\s*##g; + s#Cursor Agent##g; + s#Cursor IDE##g; + s#cursoragent\@cursor\.com##g; + s#B26\\#CONTOSO\\#g; + s#B26/#CONTOSO/#g; + s#K6A-DC\d#WIN-DC01#g; + s#UNMS Kalina#Example Site#g; + s#k\.khodasevich#user1#g; + s#\bkalinamall\b#example#g; + s#\bpapatramp\b#jdoe#g; + ' "$f" +done < <(find "$WORK" -type f -print0) + +HITS=0 +while IFS= read -r -d '' f; do + case "$f" in + *.md|*.py|*.ts|*.vue|*.json|*.sh|*.ps1|*.yml|*.yaml|*.example|*.service|*.txt|*.html|*.css|*.kt|*.kts|*.xml|*.properties|*.gradle|*.pro) ;; + *) continue ;; + esac + if grep -nE 'git\.kalinamall\.ru|git\.papatramp\.ru|papatramp\.lan|192\.168\.[0-9]+\.[0-9]+|cursoragent@cursor\.com|Co-authored-by:[[:space:]]*Cursor|Made-with:[[:space:]]*Cursor|\bkalinamall\b|\bpapatramp\b|K6A-DC|B26\\\\' "$f" >/dev/null 2>&1; then + echo "FORBIDDEN in ${f#$WORK/}" >&2 + grep -nE 'git\.kalinamall\.ru|git\.papatramp\.ru|papatramp\.lan|192\.168\.[0-9]+\.[0-9]+|cursoragent@cursor\.com|Co-authored-by:[[:space:]]*Cursor|Made-with:[[:space:]]*Cursor|\bkalinamall\b|\bpapatramp\b|K6A-DC|B26\\\\' "$f" >&2 || true + HITS=$((HITS + 1)) + fi +done < <(find "$WORK" -type f -print0) + +if [[ "$HITS" -gt 0 ]]; then + echo "sanitization failed: $HITS file(s)" >&2 + rm -rf "$WORK" + exit 1 +fi + +cd "$WORK" +git init -b main >/dev/null +git add -A +if [[ -n "$VERSION" ]]; then + MSG="chore(github): sync public mirror ($VERSION)" +else + MSG='chore(github): sync public mirror' +fi +git -c user.name='PTah' -c user.email='papatramp@gmail.com' commit -m "$MSG" >/dev/null +git remote add target "$REMOTE_URL" +git push target main --force +echo "OK -> $REMOTE_URL (orphan, force)" + +cd "$SOURCE_REPO" +rm -rf "$WORK" \ No newline at end of file diff --git a/scripts/push-home-mirror.sh b/scripts/push-home-mirror.sh new file mode 100644 index 0000000..6e2209b --- /dev/null +++ b/scripts/push-home-mirror.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# Mirror kalinamall/main to remote "home" with papatramp URLs (force). +set -euo pipefail + +SOURCE_REPO="${1:-$(pwd)}" +BRANCH="${2:-main}" +REMOTE_NAME="${3:-home}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REWRITE="$SCRIPT_DIR/rewrite-git-host-urls.sh" +SOURCE_REPO="$(cd "$SOURCE_REPO" && pwd)" +cd "$SOURCE_REPO" + +REPO_NAME="$(basename "$(git rev-parse --show-toplevel)")" +REMOTE_URL="$(git remote get-url "$REMOTE_NAME")" +WORK="${TMPDIR:-/tmp}/home-mirror-${REPO_NAME}-$$" + +rm -rf "$WORK" +mkdir -p "$WORK" +git archive --format=tar "$BRANCH" | tar -x -C "$WORK" +bash "$REWRITE" home "$WORK" + +cd "$WORK" +git init -b main >/dev/null +git add -A +SHA="$(git -C "$SOURCE_REPO" rev-parse --short HEAD)" +git -c user.name='PTah' -c user.email='papatramp@gmail.com' \ + commit -m "chore(home): mirror from kalinamall ($SHA) with papatramp URLs" >/dev/null +git remote add target "$REMOTE_URL" +git push target main --force +echo "OK -> $REMOTE_URL (home mirror, force)" + +cd "$SOURCE_REPO" +rm -rf "$WORK" \ No newline at end of file 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/rewrite-git-host-urls.sh b/scripts/rewrite-git-host-urls.sh new file mode 100644 index 0000000..6cd0a86 --- /dev/null +++ b/scripts/rewrite-git-host-urls.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# Rewrite cross-repo git host URLs in tracked text files. +# Usage: rewrite-git-host-urls.sh github|kalinamall|home|papatramp [root] +set -euo pipefail + +TARGET="${1:-}" +ROOT="${2:-$(pwd)}" +if [[ -z "$TARGET" ]]; then + echo "Usage: $0 github|kalinamall|home|papatramp [root]" >&2 + exit 2 +fi +if [[ "$TARGET" == "home" ]]; then TARGET=papatramp; fi +ROOT="$(cd "$ROOT" && pwd)" +cd "$ROOT" + +case "$TARGET" in + github) + BASE='https://github.com/PTah' + BLOB='/blob/main' + SSHTO='https://git.papatramp.ru/PapaTramp/' + ;; + kalinamall) + BASE='https://git.kalinamall.ru/PapaTramp' + BLOB='/src/branch/main' + SSHTO='ssh://git@git.papatramp.ru:2222/PapaTramp/' + ;; + papatramp) + BASE='https://git.papatramp.ru/PapaTramp' + BLOB='/src/branch/main' + SSHTO='ssh://git@git.papatramp.ru:2222/PapaTramp/' + ;; + *) + echo "Unknown target: $TARGET" >&2 + exit 2 + ;; +esac + +BASE_HOST="${BASE#https://}" +N=0 + +export BASE BLOB SSHTO BASE_HOST + +rewrite_file() { + local f="$1" + local tmp + tmp="$(mktemp)" + perl -pe ' + s#https://github\.com/PTah/([^)/\x27\"\s]+)/blob/main/#$ENV{BASE}/$1$ENV{BLOB}/#g; + s#https://github\.com/PTah/([^)/\x27\"\s]+)/src/branch/main/#$ENV{BASE}/$1$ENV{BLOB}/#g; + s#https://git\.kalinamall\.ru/PapaTramp/([^)/\x27\"\s]+)/blob/main/#$ENV{BASE}/$1$ENV{BLOB}/#g; + s#https://git\.kalinamall\.ru/PapaTramp/([^)/\x27\"\s]+)/src/branch/main/#$ENV{BASE}/$1$ENV{BLOB}/#g; + s#https://git\.papatramp\.ru/PapaTramp/([^)/\x27\"\s]+)/src/branch/main/#$ENV{BASE}/$1$ENV{BLOB}/#g; + s#https://git\.papatramp\.ru/PTah/([^)/\x27\"\s]+)/src/branch/main/#$ENV{BASE}/$1$ENV{BLOB}/#g; + s#https://github\.com/PTah/#$ENV{BASE}/#g; + s#https://git\.kalinamall\.ru/PapaTramp/#$ENV{BASE}/#g; + s#https://git\.papatramp\.ru/PapaTramp/#$ENV{BASE}/#g; + s#https://git\.papatramp\.ru/PTah/#$ENV{BASE}/#g; + s#ssh://git\@git\.kalinamall\.ru:2222/PapaTramp/#$ENV{SSHTO}#g; + s#ssh://git\@git\.papatramp\.ru:2222/PapaTramp/#$ENV{SSHTO}#g; + s#github\.com/PTah/#$ENV{BASE_HOST}/#g; + s#git\.kalinamall\.ru/PapaTramp/#$ENV{BASE_HOST}/#g; + s#git\.papatramp\.ru/PapaTramp/#$ENV{BASE_HOST}/#g; + s#git\.papatramp\.ru/PTah/#$ENV{BASE_HOST}/#g; + ' "$f" > "$tmp" + if ! cmp -s "$f" "$tmp"; then + mv "$tmp" "$f" + echo "updated: $f" + N=$((N + 1)) + else + rm -f "$tmp" + fi +} + +mapfile -t FILES < <(git -C "$ROOT" ls-files '*.md' '*.json' '*.service' '*.example' '*.sh' '*.ps1' '*.yml' '*.yaml' '*.txt' '*.kt' '*.kts' '*.xml' '*.html' '*.css' '*.ts' '*.vue' '*.py' 2>/dev/null || true) +if [[ ${#FILES[@]} -eq 0 ]]; then + mapfile -t FILES < <(find "$ROOT" -type f \( -name '*.md' -o -name '*.ps1' -o -name '*.sh' -o -name '*.yml' -o -name '*.yaml' -o -name '*.json' -o -name '*.txt' \) ! -path '*/.git/*' ! -path '*/node_modules/*' || true) +fi + +for f in "${FILES[@]}"; do + [[ -f "$f" ]] || f="$ROOT/$f" + [[ -f "$f" ]] || continue + rewrite_file "$f" +done + +echo "Rewrite-GitHostUrls: target=$TARGET base=$BASE files_changed=$N" diff --git a/scripts/setup-git-ssh.sh b/scripts/setup-git-ssh.sh new file mode 100644 index 0000000..e17deb0 --- /dev/null +++ b/scripts/setup-git-ssh.sh @@ -0,0 +1,198 @@ +#!/usr/bin/env bash +# Генерирует SSH-ключи и ~/.ssh/config для Git-хостов из local/git-ssh-hosts.conf. +# Совместим с macOS bash 3.2 (без declare -A). +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"; } + +cfg_key() { printf '%s' "$1" | tr '.-' '__'; } + +set_profile_field() { + local profile="$1" field="$2" value="$3" + eval "__pf_$(cfg_key "$profile")_${field}=\"\$value\"" +} + +get_profile_field() { + local profile="$1" field="$2" + eval "printf '%s' \"\${__pf_$(cfg_key "$profile")_${field}:-}\"" +} + +set_host_field() { + local host="$1" field="$2" value="$3" + eval "__hf_$(cfg_key "$host")_${field}=\"\$value\"" +} + +get_host_field() { + local host="$1" field="$2" + eval "printf '%s' \"\${__hf_$(cfg_key "$host")_${field}:-}\"" +} + +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" + +PROFILE_LIST=() +HOST_LIST=() +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]}" + PROFILE_LIST+=("$current_name") + continue + fi + if [[ "$line" =~ ^\[host:(.+)\]$ ]]; then + current_section='host' + current_name="${BASH_REMATCH[1]}" + HOST_LIST+=("$current_name") + set_host_field "$current_name" HostName '' + 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|KeyComment|GiteaConfig) set_profile_field "$current_name" "$key" "$value" ;; + esac + elif [[ "$current_section" == 'host' ]]; then + case "$key" in + HostName|Port|User|Profile) set_host_field "$current_name" "$key" "$value" ;; + esac + fi +done < "$HOSTS_CONFIG" + +KEYS_SEEN=() + +for profile in "${PROFILE_LIST[@]}"; do + key_name="$(get_profile_field "$profile" KeyName)" + [[ -z "$key_name" ]] && continue + + seen=0 + for existing in "${KEYS_SEEN[@]:-}"; do + [[ "$existing" == "$key_name" ]] && seen=1 && break + done + [[ "$seen" -eq 1 ]] && continue + KEYS_SEEN+=("$key_name") + + key_path="$SSH_DIR/$key_name" + comment="$(get_profile_field "$profile" KeyComment)" + comment="${comment:-$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 block_file + tmp="$(mktemp)" + block_file="$(mktemp)" + + printf '%s\n' "$block" > "$block_file" + + if [[ -f "$SSH_CONFIG" ]]; then + awk -v begin="$begin" -v end="$end" -v blockfile="$block_file" ' + $0 == begin { skip=1; next } + $0 == end { skip=0; next } + skip { next } + { print } + END { + if (blockfile != "") { + while ((getline line < blockfile) > 0) { + print line + } + close(blockfile) + } + } + ' "$SSH_CONFIG" > "$tmp" + else + cp "$block_file" "$tmp" + fi + + rm -f "$block_file" + + if [[ "$WHAT_IF" != '1' ]]; then + mv "$tmp" "$SSH_CONFIG" + chmod 600 "$SSH_CONFIG" + else + rm -f "$tmp" + fi +} + +IFS=$'\n' +sorted_hosts=($(printf '%s\n' "${HOST_LIST[@]}" | sort)) +unset IFS + +for host_alias in "${sorted_hosts[@]}"; do + profile="$(get_host_field "$host_alias" Profile)" + key_name="$(get_profile_field "$profile" KeyName)" + 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 $(get_host_field "$host_alias" HostName) + User $(get_host_field "$host_alias" User) + Port $(get_host_field "$host_alias" Port) + 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 "${KEYS_SEEN[@]:-}"; do + [[ -f "$SSH_DIR/$key_name" ]] && ssh-add "$SSH_DIR/$key_name" >/dev/null 2>&1 || true + done +fi + +log_ok 'SSH setup complete'