Compare commits

..

1 Commits

Author SHA1 Message Date
PapaTramp fb0d4597b5 chore(home): mirror from kalinamall (4507ee0) with papatramp URLs 2026-07-14 20:53:40 +10:00
39 changed files with 2296 additions and 341 deletions
-28
View File
@@ -1,28 +0,0 @@
---
description: Перед работой — прочитай все применимые rules
alwaysApply: true
---
# Всегда читай правила
Перед **любой** задачей (код, git, push, релиз, ответ пользователю):
1. **Прочитай все применимые rules** — user rules, workspace `.cursor/rules/`, особенно с `alwaysApply: true`.
2. **Не начинай работу**, пока не понял ограничения из rules (git, Cursor в репо, version-bump, язык ответа и т.д.).
3. **При конфликте** запроса пользователя и rule — остановись, объясни конфликт, спроси подтверждение.
## Обязательные проверки по типу задачи
| Задача | Перед действием прочитай / проверь |
|--------|-------------------------------------|
| `git commit` / `push` | `no-cursor-in-repositories` — нет `.cursor/` в staged; `.gitignore` содержит `.cursor/` |
| Push в `home` | `gitea-home-api` — сначала API, потом SSH push |
| Bump версии | `version-bump` (+ `version-bump-rdp` / `version-bump-ssh` для агентских мониторов) |
| Коммит | `git log -1 --format=fuller` — нет `cursoragent`, `Co-authored-by: Cursor` |
## Если rule нарушен
- **Не push**, не «доделывай быстрее».
- Исправь (amend / filter / commit-tree), затем снова проверь лог.
Правила важнее скорости выполнения задачи.
-24
View File
@@ -1,24 +0,0 @@
---
description: Git, контекст файлов и формат ответов
alwaysApply: true
---
# Контекст и дисциплина
## Git
- **Не** `git commit` / **не** `git push` без явной команды пользователя.
- Перед commit: покажи `git diff --stat`, дождись подтверждения.
- Conventional Commits: `feat:`, `fix:`, `docs:`, `refactor:`.
## Контекст
- Файлы: `@`, открытые вкладки, явные пути — не сканируй весь репо без запроса.
- Не грузи файлы >500 строк без необходимости.
- Локальные копии; не качай пакеты/URL без ошибки сборки или явного запроса.
## Ответы
- Язык: **русский** (если пользователь не переключился).
- По коду — diff/фрагменты, не дублируй неизменённые блоки.
- Без воды, но достаточно полно для понимания (не обрезай сложные ответы до одной строки).
@@ -1,18 +0,0 @@
---
description: Bump версии RDP/SSH агентов — иначе ПК не обновятся
globs: **/Login_Monitor.ps1,**/Deploy-LoginMonitor.ps1,**/Sac-Client.ps1,**/ssh-monitor,**/sac-client.sh,**/update_ssh_monitor.sh
alwaysApply: false
---
# Версии агентских мониторов
Любая **существенная правка** без bump **не доедет** до компьютеров.
| Стек | Файлы | Поле / файл версии |
|------|-------|-------------------|
| RDP | `Login_Monitor.ps1`, `version.txt` | `$ScriptVersion` = `version.txt` |
| SSH | `ssh-monitor`, `version.txt` | `SSH_MONITOR_VERSION` = `version.txt` |
Подробности: `version-bump-rdp.mdc`, `version-bump-ssh.mdc`.
**SAC backend:** `backend/app/version.py` (`APP_VERSION`) — отдельный релиз, на агенты ПК не влияет.
-12
View File
@@ -1,12 +0,0 @@
---
description: Краткий C# без лишних токенов в выводе
globs: **/*.cs
alwaysApply: false
---
# C# — компактный вывод
- File-scoped namespaces, pattern matching, target-typed `new()`, primary constructors где уместно.
- Без XML-комментариев, `#region`, лишних `using`.
- Без `Console.WriteLine` / debug-log, если задача не про логирование.
- Expression-bodied members, где читаемо.
-37
View File
@@ -1,37 +0,0 @@
---
description: Gitea home — репо через API, не вручную в вебе
alwaysApply: true
---
# Gitea home (papatramp)
При push в **`home`**, настройке remotes или первом заливе — **через Gitea API**. Не проси создать репо вручную при `Push to create is not enabled` / `Cannot find repository`.
## Пути (локально, не в git-репо)
| Назначение | Путь |
|------------|------|
| Конфиг API (секреты) | `$HOME\CursorRules\local\papatramp-gitea.conf` |
| Remotes | `$HOME\CursorRules\scripts\Set-GitRemotes.ps1` |
| Создать репо на home | `$HOME\CursorRules\scripts\Ensure-GiteaHomeRepo.ps1` |
`init-cursor` копирует в репо **только `*.mdc`**.
SSH **`home`**: `ssh://git@git.papatramp.lan:2222/PapaTramp/<RepoName>.git`
## Алгоритм
1. `GET {GiteaApiUrl}/api/v1/repos/PapaTramp/{RepoName}` — Basic Auth из конфига.
2. Если **404** — `POST {GiteaApiUrl}/api/v1/user/repos` (или `.../orgs/PapaTramp/repos`), тело: `{ "name": "<RepoName>", "private": true, "auto_init": false }`.
Или: `Ensure-GiteaHomeRepo.ps1 -RepoName <RepoName>`.
3. `git push home <branch>` (SSH).
## Запрещено
- «Создайте репо в Gitea сами» без попытки API.
- Коммитить `papatramp-gitea.conf`, пароли, временные скрипты с credentials.
- HTTPS/`origin` для home вместо remote **`home`**.
## API недоступен
Сообщи, что LAN/API (`192.168.128.48:3000`) недоступен — VPN/сеть. Не отправляй в веб-интерфейс.
@@ -1,50 +0,0 @@
---
description: Не коммитить .cursor/ и IDE-файлы в git-репозитории
alwaysApply: true
---
# Cursor не попадает в git
В **коммитах и tracked-файлах** запрещено добавлять артефакты Cursor как IDE/агента.
## Запрещено коммитить
| Путь | Назначение |
|------|------------|
| **`.cursor/`** | rules, hooks, hooks.json, settings — только локально |
| **`.cursorignore`** | индексация IDE, не часть продукта |
| **`AGENTS.md`**, **`CURSOR.md`** | служебные файлы IDE |
Не создавать `.cursor/hooks/` с auto-push/auto-commit в репозиториях проекта.
## Обязательно в `.gitignore` каждого репо
```gitignore
.cursor/
.cursorignore
```
При работе в репозитории: если строк нет — **добавить в том же коммите**, что и код, **не** коммитить сами `.cursor/`.
После `init-cursor` (копирование rules в проект) — rules остаются **локально** и **не попадают** в `git add`.
## Перед `git commit` / `git push`
1. `git status` — в staged **нет** `.cursor/` и `.cursorignore`.
2. **Не** использовать `git add -A` / `git add .` без проверки.
3. Если `.cursor` уже в индексе: `git rm -r --cached .cursor` и `git rm --cached .cursorignore`.
4. `git ls-files` — не должно быть путей, начинающихся с `.cursor`.
## Запрещено в тексте коммитов и docs
- Упоминания Cursor IDE, Cursor Agent, cursoragent, cursor.com, cursor.sh.
- Trailers: Co-authored-by: Cursor, Made-with: Cursor.
## Разрешено (не путать с IDE)
- CSS cursor: pointer; poll cursor, DB cursor, Console.CursorLeft, MOEX analytics.cursor.
## Git / GitHub
- Author и committer — Andrey Lutsenko, без co-author Cursor.
- Перед push в public: git log --format=fuller, поиск cursoragent / Co-authored-by: Cursor.
-12
View File
@@ -1,12 +0,0 @@
---
description: Компактный PowerShell в выводе агента
globs: **/*.{ps1,psm1}
alwaysApply: false
---
# PowerShell — компактный вывод
- Pipeline вместо многострочных `foreach`, где читаемо.
- Без comment-based help и лишних `Write-Host`, если не просили UI/логи.
- Не дублируй длинные CLI-аргументы целиком — placeholder или переменная.
- Сохраняй стиль и алиасы **существующего файла**; не навязывай `gc`/`gci` если в проекте полные имена.
-12
View File
@@ -1,12 +0,0 @@
---
description: Компактный Python в выводе агента
globs: **/*.py
alwaysApply: false
---
# Python — компактный вывод
- Comprehensions, ternary — где читаемо; stdlib (`pathlib`, `json`, `subprocess`) без лишних зависимостей.
- Type hints — только если уже приняты в файле.
- Без docstrings/комментариев «для объяснения», если не просили.
- При правках — функция/метод, не весь файл; `# ... existing code ...` для неизменных блоков.
-20
View File
@@ -1,20 +0,0 @@
---
description: Bump version.txt и $ScriptVersion в RDP-login-monitor
globs: **/Login_Monitor.ps1,**/Deploy-LoginMonitor.ps1,**/Sac-Client.ps1,**/Exchange-MailSecurity.ps1,**/Watchdog_RDP_Monitor.ps1,**/version.txt
alwaysApply: false
---
# Bump RDP-login-monitor
Без bump **`Deploy-LoginMonitor.ps1` не обновит ПК** (`version.txt` vs `deployed_version.txt`).
## В том же коммите
1. `Login_Monitor.ps1` — `$ScriptVersion` (напр. `2.0.7-SAC`)
2. `version.txt` — **та же строка**
Patch: `2.0.6-SAC` → `2.0.7-SAC`. Крупный релиз — minor по смыслу.
`Exchange-MailSecurity.ps1` — свой `$ScriptVersion`, bump только при правке этого пакета.
После bump — публикация на NETLOGON.
-18
View File
@@ -1,18 +0,0 @@
---
description: Bump version.txt и SSH_MONITOR_VERSION в ssh-monitor
globs: **/ssh-monitor,**/sac-client.sh,**/update_ssh_monitor.sh,**/version.txt
alwaysApply: false
---
# Bump ssh-monitor
Без bump **`update_ssh_monitor.sh` не скопирует скрипт** (sha256).
## В том же коммите
1. `ssh-monitor` — `SSH_MONITOR_VERSION="X.Y.Z-SAC"`
2. `version.txt` — **та же строка**
Patch по умолчанию. `sac-client.sh` версию не задаёт — берёт из установленного `ssh-monitor`.
После push: `git pull` + `update_ssh_monitor.sh` на хостах.
-14
View File
@@ -1,14 +0,0 @@
---
description: Patch/minor bump версии при осмысленных правках
alwaysApply: true
---
# Bump версии
## Patch (обычно)
После осмысленного изменения — **+0.0.1** (`0.7.0` → `0.7.1`) в **том же коммите**.
## Minor / major
При заметном релизе — по смыслу (`0.6.x` → `0.7.0`). Не обязательно на каждую мелочь.
-65
View File
@@ -1,65 +0,0 @@
# Çàâèñèìîñòè è ïàêåòû
node_modules/
bower_components/
jspm_packages/
.npm/
vendor/
.pnpm-store/
# Âèðòóàëüíûå îêðóæåíèÿ (Python)
.venv/
venv/
env/
ENV/
.env/
# Ñáîðêà, êýø è àðòåôàêòû
dist/
build/
out/
.next/
.nuxt/
.docusaurus/
.cache/
.sass-cache/
.turbo/
target/
bin/
obj/
# Ñèñòåìíûå ïàïêè IDE è ëîãè
.git/
.svn/
.idea/
.vscode/
.cursor/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Ìåäèà, øðèôòû è òÿæåëûå áèíàðíûå ôàéëû
*.png
*.jpg
*.jpeg
*.gif
*.svg
*.ico
*.mp4
*.mp3
*.pdf
*.zip
*.tar.gz
*.rar
*.exe
*.dll
*.woff
*.woff2
*.ttf
*.eot
# Ñãåíåðèðîâàííûå ôàéëû áëîêèðîâîê (îïöèîíàëüíî, ýêîíîìÿò ìíîãî òîêåíîâ)
package-lock.json
yarn.lock
pnpm-lock.yaml
poetry.lock
+21
View File
@@ -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
+13
View File
@@ -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
+2
View File
@@ -0,0 +1,2 @@
.cursor/
.cursorignore
+134 -10
View File
@@ -1,24 +1,148 @@
# Rules
Правила и скрипты для настройки нового компьютера: Cursor rules, git remotes, home Gitea.
Скрипты для 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
git clone https://git.kalinamall.ru/PapaTramp/Rules $HOME\CursorRules
# Windows — с другого ПК (пример)
scp user@work-pc:CursorRules/install.ps1 $env:TEMP\
```
Добавьте `init-cursor` в профиль PowerShell (один раз):
```powershell
. $HOME\CursorRules\init-cursor.ps1
```bash
# Mac — с другого ПК
scp user@work-pc:CursorRules/install.sh /tmp/
```
## В каждом проекте
USB, общая папка — тоже подходит.
### 2. Запустить в каталоге **вашего проекта**
```powershell
cd C:\path\to\repo
# 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
```
Скрипт копирует `.cursorignore` и `.cursor/rules/*.mdc`, при необходимости дополняет `.gitignore`, настраивает remotes (`home`, `github`, `kalinamall`) и создаёт репозиторий на home Gitea через API.
```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`.
### Cursor
На **всех** remotes (kalinamall, home, github) запрещены Cursor-trailers / cursoragent / Cursor IDE в коммитах и tracked-файлах. На github дополнительно hard sanitize хостов/PII.
+28
View File
@@ -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
+13
View File
@@ -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'
+94 -13
View File
@@ -1,32 +1,93 @@
#Requires -Version 5.1
#Requires -Version 5.1
<#
.SYNOPSIS
Копирует Cursor rules в текущий каталог и настраивает git remotes.
Инициализация Cursor в текущем проекте; на новом ПК сначала настраивает git/SSH.
.DESCRIPTION
1. .cursorignore из $HOME\CursorRules
2. .cursor/rules/*.mdc
3. В git-репо: Set-GitRemotes.ps1 + Ensure-GiteaHomeRepo.ps1 (home Gitea)
Один вызов 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 init-cursor {
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 = Join-Path $HOME 'CursorRules'
$source = Get-CursorRulesDest
$rulesSrc = Join-Path $source '.cursor\rules'
$scripts = Join-Path $source 'scripts'
if (-not (Test-Path $source)) {
Write-Host "Error: $source not found" -ForegroundColor Red
return
}
$cursorIgnore = Join-Path $source '.cursorignore'
if (Test-Path $cursorIgnore) {
Copy-Item $cursorIgnore . -Force
@@ -68,7 +129,7 @@ function init-cursor {
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
Write-Host '[!] Not a git repo - remotes skipped' -ForegroundColor Yellow
return
}
@@ -90,3 +151,23 @@ function init-cursor {
}
}
}
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
}
+143
View File
@@ -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'
}
+65
View File
@@ -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
}
+62
View File
@@ -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
+52
View File
@@ -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
+2
View File
@@ -0,0 +1,2 @@
UserName=PTah
UserEmail=papatramp@gmail.com
+9
View File
@@ -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
+210
View File
@@ -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
}
+144
View File
@@ -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*<cursoragent@cursor\.com>\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
+60
View File
@@ -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
+136
View File
@@ -0,0 +1,136 @@
#Requires -Version 5.1
<#
.SYNOPSIS
Регистрирует публичные SSH-ключи в Gitea через API (kalinamall, papatramp).
#>
[CmdletBinding()]
param(
[string]$ConfigDir = "$HOME\CursorRules\local",
[string]$HostsConfig = '',
[switch]$WhatIf
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
function Write-Step([string]$Message) { Write-Host "[*] $Message" -ForegroundColor Cyan }
function Write-Ok([string]$Message) { Write-Host "[+] $Message" -ForegroundColor Green }
function Write-Warn([string]$Message) { Write-Host "[!] $Message" -ForegroundColor Yellow }
function Read-KeyValueFile {
param([string]$Path)
$data = @{}
Get-Content -LiteralPath $Path | ForEach-Object {
if ($_ -match '^([^#=]+)=(.*)$') {
$data[$Matches[1].Trim()] = $Matches[2].Trim()
}
}
return $data
}
function Read-GitSshHostsConfig {
param([string]$Path)
$profiles = @{}
$currentProfile = $null
Get-Content -LiteralPath $Path | ForEach-Object {
$line = $_.Trim()
if (-not $line -or $line.StartsWith('#')) { return }
if ($line -match '^\[profile:(.+)\]$') {
$currentProfile = $Matches[1]
$profiles[$currentProfile] = @{}
return
}
if ($line -match '^\[host:') { $currentProfile = $null; return }
if ($line -match '^([^=]+)=(.*)$' -and $currentProfile) {
$profiles[$currentProfile][$Matches[1].Trim()] = $Matches[2].Trim()
}
}
return $profiles
}
function Get-GiteaAuthHeader {
param([hashtable]$Cfg)
$pair = '{0}:{1}' -f $Cfg.GiteaApiUser, $Cfg.GiteaApiPassword
$auth = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes($pair))
return @{ Authorization = $auth; 'Content-Type' = 'application/json' }
}
function Register-GiteaKey {
param(
[hashtable]$Cfg,
[string]$PublicKeyPath,
[string]$Title
)
if (-not $Cfg.GiteaApiPassword) {
Write-Warn "$Title : GiteaApiPassword empty - add key manually in Gitea UI"
Write-Host (Get-Content -LiteralPath $PublicKeyPath -Raw).Trim()
return
}
$base = $Cfg.GiteaApiUrl.TrimEnd('/')
$headers = Get-GiteaAuthHeader -Cfg $Cfg
$pub = (Get-Content -LiteralPath $PublicKeyPath -Raw).Trim()
$existing = Invoke-RestMethod -Method Get -Uri "$base/api/v1/user/keys" -Headers $headers -TimeoutSec 15
foreach ($item in @($existing)) {
if ($item.key.Trim() -eq $pub) {
Write-Ok "$Title : key already registered ($($item.title))"
return
}
}
$body = (@{ title = $Title; key = $pub } | ConvertTo-Json)
if ($WhatIf) {
Write-Step "WhatIf: POST $base/api/v1/user/keys ($Title)"
return
}
$created = Invoke-RestMethod -Method Post -Uri "$base/api/v1/user/keys" -Headers $headers -Body $body -TimeoutSec 30
Write-Ok "$Title : registered ($($created.title))"
}
if (-not $HostsConfig) {
$HostsConfig = Join-Path $ConfigDir 'git-ssh-hosts.conf'
}
if (-not (Test-Path -LiteralPath $HostsConfig)) {
throw "Config not found: $HostsConfig"
}
$profiles = Read-GitSshHostsConfig -Path $HostsConfig
$sshDir = Join-Path $HOME '.ssh'
$machine = $env:COMPUTERNAME
if (-not $machine) { $machine = [Environment]::MachineName }
$registered = @{}
foreach ($profileName in $profiles.Keys) {
$profile = $profiles[$profileName]
$giteaConfigName = $profile.GiteaConfig
if (-not $giteaConfigName) { continue }
$keyName = $profile.KeyName
if ($registered.ContainsKey($keyName)) { continue }
$registered[$keyName] = $true
$giteaPath = Join-Path $ConfigDir $giteaConfigName
if (-not (Test-Path -LiteralPath $giteaPath)) {
Write-Warn "Gitea config not found: $giteaPath"
continue
}
$cfg = Read-KeyValueFile -Path $giteaPath
$pubPath = Join-Path $sshDir "$keyName.pub"
if (-not (Test-Path -LiteralPath $pubPath)) {
Write-Warn "Public key not found: $pubPath (run Setup-GitSsh first)"
continue
}
$title = "$machine-$keyName"
Write-Step "Registering $keyName on $($cfg.GiteaHost)"
Register-GiteaKey -Cfg $cfg -PublicKeyPath $pubPath -Title $title
}
Write-Ok 'Gitea SSH key registration done'
+91
View File
@@ -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"
+43 -8
View File
@@ -1,7 +1,7 @@
#Requires -Version 5.1
<#
.SYNOPSIS
Стандартные git remote: home, kalinamall, github.
Стандартные git remote: home (papatramp), kalinamall, github (PTah).
.EXAMPLE
cd C:\Users\papat\Projects\MyRepo
@@ -29,19 +29,46 @@ function Test-GitRepo {
}
}
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
$existing = @(git remote 2>$null)
$has = $existing -contains $Name
if ($WhatIf) {
@@ -60,9 +87,19 @@ function Set-Remote {
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 = @('origin')
github = @()
kalinamall = @('gitea-kalinamall', 'kalina')
}
foreach ($old in $legacy[$Name]) {
@@ -81,16 +118,14 @@ function Set-Remote {
Test-GitRepo
$repoName = Get-RepoName -Name $Repo
$homeUrl = "ssh://git@git.papatramp.lan:2222/${Owner}/${repoName}.git"
$kalinamallUrl = "ssh://git@git.kalinamall.lan:2222/${Owner}/${repoName}.git"
$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 'github' -Url $githubUrl
if (-not $SkipKalinamall) {
Set-Remote -Name 'kalinamall' -Url $kalinamallUrl
}
Set-Remote -Name 'github' -Url $githubUrl
Write-Host ''
Write-Host 'Remotes:' -ForegroundColor Magenta
+110
View File
@@ -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
+192
View File
@@ -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'
+128
View File
@@ -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'
+92
View File
@@ -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*<cursoragent\@cursor\.com>\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"
+33
View File
@@ -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"
+136
View File
@@ -0,0 +1,136 @@
#!/usr/bin/env bash
# Регистрирует публичные SSH-ключи в Gitea через API.
set -euo pipefail
CONFIG_DIR="${CONFIG_DIR:-$HOME/CursorRules/local}"
HOSTS_CONFIG="${HOSTS_CONFIG:-$CONFIG_DIR/git-ssh-hosts.conf}"
WHAT_IF="${WHAT_IF:-0}"
log_step() { printf '[*] %s\n' "$1"; }
log_ok() { printf '[+] %s\n' "$1"; }
log_warn() { printf '[!] %s\n' "$1"; }
if [[ ! -f "$HOSTS_CONFIG" ]]; then
echo "Config not found: $HOSTS_CONFIG" >&2
exit 1
fi
read_kv_file() {
local file="$1"
local line key value
while IFS= read -r line || [[ -n "$line" ]]; do
[[ "$line" =~ ^[[:space:]]*# ]] && continue
[[ "$line" != *=* ]] && continue
key="${line%%=*}"
value="${line#*=}"
key="${key%"${key##*[![:space:]]}"}"
value="${value#"${value%%[![:space:]]*}"}"
printf '%s=%s\n' "$key" "$value"
done < "$file"
}
declare -A PROFILE_KEYNAME PROFILE_GITEA
current_section=''
current_name=''
while IFS= read -r line || [[ -n "$line" ]]; do
line="${line%%#*}"
line="${line%"${line##*[![:space:]]}"}"
[[ -z "$line" ]] && continue
if [[ "$line" =~ ^\[profile:(.+)\]$ ]]; then
current_section='profile'
current_name="${BASH_REMATCH[1]}"
continue
fi
if [[ "$line" =~ ^\[host: ]]; then
current_section=''
continue
fi
[[ "$line" != *=* || "$current_section" != 'profile' ]] && continue
key="${line%%=*}"
value="${line#*=}"
key="${key%"${key##*[![:space:]]}"}"
value="${value#"${value%%[![:space:]]*}"}"
case "$key" in
KeyName) PROFILE_KEYNAME["$current_name"]="$value" ;;
GiteaConfig) PROFILE_GITEA["$current_name"]="$value" ;;
esac
done < "$HOSTS_CONFIG"
SSH_DIR="$HOME/.ssh"
MACHINE="$(hostname -s 2>/dev/null || hostname)"
declare -A REGISTERED
register_gitea_key() {
local cfg_file="$1"
local pub_path="$2"
local title="$3"
declare -A CFG=()
local kv key value
while IFS= read -r kv; do
key="${kv%%=*}"
value="${kv#*=}"
CFG["$key"]="$value"
done < <(read_kv_file "$cfg_file")
if [[ -z "${CFG[GiteaApiPassword]:-}" ]]; then
log_warn "$title : GiteaApiPassword empty — add key manually in Gitea UI"
cat "$pub_path"
return 0
fi
local base="${CFG[GiteaApiUrl]%/}"
local auth user pass pub body response
user="${CFG[GiteaApiUser]}"
pass="${CFG[GiteaApiPassword]}"
auth="$(printf '%s:%s' "$user" "$pass" | base64 -w0 2>/dev/null || printf '%s:%s' "$user" "$pass" | base64)"
pub="$(tr -d '\r\n' < "$pub_path")"
response="$(curl -fsS -H "Authorization: Basic $auth" "$base/api/v1/user/keys" 2>/dev/null || true)"
if [[ -n "$response" ]] && printf '%s' "$response" | grep -Fq "$pub"; then
log_ok "$title : key already registered"
return 0
fi
body="$(printf '{"title":"%s","key":"%s"}' "$title" "$pub")"
if [[ "$WHAT_IF" == '1' ]]; then
log_step "WhatIf: POST $base/api/v1/user/keys ($title)"
return 0
fi
curl -fsS -X POST -H "Authorization: Basic $auth" -H 'Content-Type: application/json' \
-d "$body" "$base/api/v1/user/keys" >/dev/null
log_ok "$title : registered"
}
for profile in "${!PROFILE_GITEA[@]}"; do
gitea_config="${PROFILE_GITEA[$profile]}"
[[ -z "$gitea_config" ]] && continue
key_name="${PROFILE_KEYNAME[$profile]}"
[[ -z "$key_name" || -n "${REGISTERED[$key_name]:-}" ]] && continue
REGISTERED["$key_name"]=1
gitea_path="$CONFIG_DIR/$gitea_config"
pub_path="$SSH_DIR/$key_name.pub"
if [[ ! -f "$gitea_path" ]]; then
log_warn "Gitea config not found: $gitea_path"
continue
fi
if [[ ! -f "$pub_path" ]]; then
log_warn "Public key not found: $pub_path (run setup-git-ssh.sh first)"
continue
fi
title="$MACHINE-$key_name"
log_step "Registering $key_name"
register_gitea_key "$gitea_path" "$pub_path" "$title"
done
log_ok 'Gitea SSH key registration done'
+85
View File
@@ -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"
+198
View File
@@ -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'