Compare commits

..
Author SHA1 Message Date
github-actions[bot] 7a98fed138 Auto-build: roo (python) from 77c797e 2026-09-19 18:31:32 +00:00
5885 changed files with 8430 additions and 344138 deletions
-32
View File
@@ -1,32 +0,0 @@
{
"name": "cc-1c-skills",
"interface": {
"displayName": "1C Skills"
},
"plugins": [
{
"name": "1c-skills",
"source": {
"source": "url",
"url": "https://github.com/Nikolay-Shirokov/cc-1c-skills.git",
"ref": "port-codex"
},
"policy": {
"installation": "AVAILABLE"
},
"category": "Development"
},
{
"name": "1c-skills-py",
"source": {
"source": "url",
"url": "https://github.com/Nikolay-Shirokov/cc-1c-skills.git",
"ref": "port-codex-py"
},
"policy": {
"installation": "AVAILABLE"
},
"category": "Development"
}
]
}
-24
View File
@@ -1,24 +0,0 @@
{
"$schema": "https://json.schemastore.org/claude-code-marketplace-manifest.json",
"name": "cc-1c-skills",
"description": "Маркетплейс навыков для разработки на платформе 1С:Предприятие",
"owner": {
"name": "Nikolay Shirokov"
},
"plugins": [
{
"name": "1c-skills",
"source": "./",
"description": "[PowerShell] Навыки для разработки на 1С:Предприятие 8.3 — абстракции над XML-форматами и CLI конфигуратора, плюс глаза и руки для тестирования через веб-клиент."
},
{
"name": "1c-skills-py",
"source": {
"source": "github",
"repo": "Nikolay-Shirokov/cc-1c-skills",
"ref": "port-claude-code-py"
},
"description": "[Python] То же — для Linux/Mac или когда PowerShell недоступен."
}
]
}
-31
View File
@@ -1,31 +0,0 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "1c-skills",
"description": "[PowerShell] Навыки для разработки на 1С:Предприятие 8.3 — абстракции над XML-форматами и CLI конфигуратора, плюс глаза и руки для тестирования через веб-клиент.",
"author": {
"name": "Nikolay Shirokov"
},
"homepage": "https://github.com/Nikolay-Shirokov/cc-1c-skills",
"repository": "https://github.com/Nikolay-Shirokov/cc-1c-skills",
"license": "MIT",
"keywords": [
"1c",
"1c-dev",
"cf",
"cfe",
"epf",
"erf",
"metadata",
"configuration",
"extension",
"form",
"report",
"skd",
"data-processor",
"mxl",
"web-client",
"testing",
"test-automation"
],
"skills": "./.claude/skills/"
}
-7
View File
@@ -1,7 +0,0 @@
# Коммиты, которые git blame должен «проскакивать» (механические правки —
# не меняют авторство содержимого). Включить локально:
# git config blame.ignoreRevsFile .git-blame-ignore-revs
# GitHub/GitLab уважают этот файл в blame-UI автоматически.
# chore(repo): нормализация EOL к LF + .gitattributes
26888a07d58351755fb8e487727c00d5b611eb95
-28
View File
@@ -1,28 +0,0 @@
# EOL policy
# ─────────────────────────────────────────────────────────────────────────────
# Авторский контент нормализуем к LF: инструмент правки (Edit) всегда пишет LF,
# поэтому единый LF убирает EOL-шум в диффах и ловушку «не правь CRLF-файл».
# git с eol=lf конвертит ТОЛЬКО CR<->LF и не трогает BOM (BOM — байты контента),
# поэтому BOM на .ps1 сохраняется.
*.ps1 text eol=lf
*.psm1 text eol=lf
*.py text eol=lf
*.mjs text eol=lf
*.md text eol=lf
*.json text eol=lf
.gitignore text eol=lf
# .bsl уже целиком LF — пин фиксирует статус-кво от будущего дрейфа.
*.bsl text eol=lf
# Данные 1С НЕ трогаем. *.xml — реальные выгрузки 1С (EOL местами значим,
# правим не мы, а навыки): оставляем как есть, под управление не берём.
# autocrlf=false и отсутствие text-атрибута => git хранит их байты как есть.
# Бинарники 1С
*.bin binary
# Package.bin пакетов XDTO — текстовый XML, несмотря на расширение. Оставляем
# под правилом *.bin binary (байты не нормализуются), но включаем текстовый diff,
# иначе изменение модели пакета в истории выглядит как «Binary files differ».
XDTOPackages/**/Package.bin diff
-26
View File
@@ -1,26 +0,0 @@
# 1C Skills for {{PLATFORM_LABEL}} ({{RUNTIME_LABEL}})
Автоматическая сборка из [main]({{MAIN_REPO_URL}}) — навыки 1С:Предприятие 8.3 для AI-агента **{{PLATFORM_LABEL}}** с рантаймом **{{RUNTIME_LABEL}}**.
> Эта ветка генерируется CI на каждый push в main. **Не редактируйте напрямую** — все правки идут в [main]({{MAIN_REPO_URL}}).
## Установка
1. Скачайте ZIP этой ветки: **Code → Download ZIP** (или `git archive`).
2. Распакуйте в корень своего проекта — должна появиться папка `{{PLATFORM_DIR}}/`.
3. Запустите {{PLATFORM_LABEL}} из этого проекта — навыки станут доступны.
## Требования
{{RUNTIME_REQUIREMENTS}}
- **1С:Предприятие 8.3** — для сборки/разборки EPF/ERF и работы с базами.
- **Node.js 18+** — для `/web-test`.
## Документация
Полные гайды, спецификации и описание навыков — в [main]({{MAIN_REPO_URL}}).
---
Source: {{MAIN_REPO_URL}}
Build commit: `{{COMMIT_SHA}}`
-31
View File
@@ -1,31 +0,0 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "{{PLUGIN_NAME}}",
"description": "[Python] Навыки для разработки на 1С:Предприятие 8.3 — абстракции над XML-форматами и CLI конфигуратора, плюс глаза и руки для тестирования через веб-клиент. Linux/Mac или когда PowerShell недоступен.",
"author": {
"name": "Nikolay Shirokov"
},
"homepage": "https://github.com/Nikolay-Shirokov/cc-1c-skills",
"repository": "https://github.com/Nikolay-Shirokov/cc-1c-skills",
"license": "MIT",
"keywords": [
"1c",
"1c-dev",
"cf",
"cfe",
"epf",
"erf",
"metadata",
"configuration",
"extension",
"form",
"report",
"skd",
"data-processor",
"mxl",
"web-client",
"testing",
"test-automation"
],
"skills": "./.claude/skills/"
}
-36
View File
@@ -1,36 +0,0 @@
{
"name": "{{PLUGIN_NAME}}",
"version": "{{VERSION}}",
"description": "[{{RUNTIME_LABEL}}] Навыки для разработки на 1С:Предприятие 8.3 — абстракции над XML-форматами и CLI конфигуратора, плюс глаза и руки для тестирования через веб-клиент.",
"author": {
"name": "Nikolay Shirokov"
},
"homepage": "https://github.com/Nikolay-Shirokov/cc-1c-skills",
"repository": "https://github.com/Nikolay-Shirokov/cc-1c-skills",
"license": "MIT",
"keywords": [
"1c",
"1c-dev",
"cf",
"cfe",
"epf",
"erf",
"metadata",
"configuration",
"extension",
"form",
"report",
"skd",
"data-processor",
"mxl",
"web-client",
"testing",
"test-automation"
],
"skills": "./.codex/skills/",
"interface": {
"displayName": "1C Skills ({{RUNTIME_LABEL}})",
"shortDescription": "{{SHORT_DESCRIPTION}}",
"category": "Development"
}
}
-246
View File
@@ -1,246 +0,0 @@
name: Build port branches
on:
push:
branches: [main]
paths:
- '.claude/skills/**'
- 'scripts/switch.py'
- 'requirements.txt'
- '.github/templates/README.port.md.tmpl'
- '.github/templates/codex-plugin.json.tmpl'
- '.github/templates/claude-plugin.json.tmpl'
- '.github/workflows/build-ports.yml'
- 'LICENSE'
workflow_dispatch:
permissions:
contents: write
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- platform: claude-code
runtime: python
branch: port-claude-code-py
label: Claude Code
target_dir: .claude/skills
- platform: cursor
runtime: powershell
branch: port-cursor
label: Cursor
target_dir: .cursor/skills
- platform: cursor
runtime: python
branch: port-cursor-py
label: Cursor
target_dir: .cursor/skills
- platform: codex
runtime: powershell
branch: port-codex
label: Codex
target_dir: .codex/skills
- platform: codex
runtime: python
branch: port-codex-py
label: Codex
target_dir: .codex/skills
- platform: copilot
runtime: powershell
branch: port-copilot
label: GitHub Copilot
target_dir: .github/skills
- platform: copilot
runtime: python
branch: port-copilot-py
label: GitHub Copilot
target_dir: .github/skills
- platform: augment
runtime: powershell
branch: port-augment
label: Augment
target_dir: .augment/skills
- platform: augment
runtime: python
branch: port-augment-py
label: Augment
target_dir: .augment/skills
- platform: cline
runtime: powershell
branch: port-cline
label: Cline
target_dir: .cline/skills
- platform: cline
runtime: python
branch: port-cline-py
label: Cline
target_dir: .cline/skills
- platform: kilo
runtime: powershell
branch: port-kilo
label: Kilo Code
target_dir: .kilocode/skills
- platform: kilo
runtime: python
branch: port-kilo-py
label: Kilo Code
target_dir: .kilocode/skills
- platform: kiro
runtime: powershell
branch: port-kiro
label: Kiro
target_dir: .kiro/skills
- platform: kiro
runtime: python
branch: port-kiro-py
label: Kiro
target_dir: .kiro/skills
- platform: gemini
runtime: powershell
branch: port-gemini
label: Gemini CLI
target_dir: .gemini/skills
- platform: gemini
runtime: python
branch: port-gemini-py
label: Gemini CLI
target_dir: .gemini/skills
- platform: opencode
runtime: powershell
branch: port-opencode
label: OpenCode
target_dir: .opencode/skills
- platform: opencode
runtime: python
branch: port-opencode-py
label: OpenCode
target_dir: .opencode/skills
- platform: roo
runtime: powershell
branch: port-roo
label: Roo Code
target_dir: .roo/skills
- platform: roo
runtime: python
branch: port-roo-py
label: Roo Code
target_dir: .roo/skills
- platform: windsurf
runtime: powershell
branch: port-windsurf
label: Windsurf
target_dir: .windsurf/skills
- platform: windsurf
runtime: python
branch: port-windsurf-py
label: Windsurf
target_dir: .windsurf/skills
- platform: codeassistant
runtime: powershell
branch: port-codeassistant
label: Yandex Code Assistant
target_dir: .codeassistant/skills
- platform: codeassistant
runtime: python
branch: port-codeassistant-py
label: Yandex Code Assistant
target_dir: .codeassistant/skills
- platform: agents
runtime: powershell
branch: port-agents
label: Agent Skills
target_dir: .agents/skills
- platform: agents
runtime: python
branch: port-agents-py
label: Agent Skills
target_dir: .agents/skills
steps:
- name: Checkout main
uses: actions/checkout@v5
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.11'
- name: Build skills tree for ${{ matrix.platform }} (${{ matrix.runtime }})
run: |
python scripts/switch.py "${{ matrix.platform }}" \
--project-dir build \
--runtime "${{ matrix.runtime }}"
- name: Render port README
env:
PLATFORM_LABEL: ${{ matrix.label }}
PLATFORM_DIR: ${{ matrix.target_dir }}
RUNTIME: ${{ matrix.runtime }}
RUNTIME_LABEL: ${{ matrix.runtime == 'powershell' && 'PowerShell' || 'Python' }}
COMMIT_SHA: ${{ github.sha }}
MAIN_REPO_URL: https://github.com/${{ github.repository }}
run: |
if [ "$RUNTIME" = "powershell" ]; then
RUNTIME_REQUIREMENTS='- **Windows** с PowerShell 5.1+ (входит в Windows).'
else
RUNTIME_REQUIREMENTS='- **Python 3.9+**. Установка зависимостей: `pip install -r requirements.txt` (lxml, Pillow, psutil).'
fi
sed \
-e "s|{{PLATFORM_LABEL}}|${PLATFORM_LABEL}|g" \
-e "s|{{PLATFORM_DIR}}|${PLATFORM_DIR}|g" \
-e "s|{{RUNTIME_LABEL}}|${RUNTIME_LABEL}|g" \
-e "s|{{RUNTIME_REQUIREMENTS}}|${RUNTIME_REQUIREMENTS}|g" \
-e "s|{{COMMIT_SHA}}|${COMMIT_SHA}|g" \
-e "s|{{MAIN_REPO_URL}}|${MAIN_REPO_URL}|g" \
.github/templates/README.port.md.tmpl > build/README.md
- name: Render Codex plugin manifest
if: matrix.platform == 'codex'
env:
PLUGIN_NAME: ${{ matrix.runtime == 'python' && '1c-skills-py' || '1c-skills' }}
RUNTIME_LABEL: ${{ matrix.runtime == 'powershell' && 'PowerShell' || 'Python' }}
SHORT_DESCRIPTION: ${{ matrix.runtime == 'python' && 'Python runtime (Linux/Mac/Windows)' || 'PowerShell runtime (Windows-first)' }}
COMMIT_SHA: ${{ github.sha }}
run: |
VERSION="$(date -u +%Y.%-m.%-d)+${COMMIT_SHA::7}"
mkdir -p build/.codex-plugin
sed \
-e "s|{{PLUGIN_NAME}}|${PLUGIN_NAME}|g" \
-e "s|{{VERSION}}|${VERSION}|g" \
-e "s|{{RUNTIME_LABEL}}|${RUNTIME_LABEL}|g" \
-e "s|{{SHORT_DESCRIPTION}}|${SHORT_DESCRIPTION}|g" \
.github/templates/codex-plugin.json.tmpl > build/.codex-plugin/plugin.json
- name: Render Claude plugin manifest (Py variant)
if: matrix.platform == 'claude-code' && matrix.runtime == 'python'
env:
PLUGIN_NAME: 1c-skills-py
run: |
mkdir -p build/.claude-plugin
sed -e "s|{{PLUGIN_NAME}}|${PLUGIN_NAME}|g" \
.github/templates/claude-plugin.json.tmpl > build/.claude-plugin/plugin.json
- name: Copy LICENSE
run: cp LICENSE build/LICENSE
- name: Copy requirements.txt (Python builds only)
if: matrix.runtime == 'python'
run: cp requirements.txt build/requirements.txt
- name: Force-push orphan snapshot to ${{ matrix.branch }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
cd build
git init -q -b master
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add -A
git commit -q -m "Auto-build: ${{ matrix.platform }} (${{ matrix.runtime }}) from ${GITHUB_SHA::7}"
git push --force \
"https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" \
"master:${{ matrix.branch }}"
-58
View File
@@ -1,58 +0,0 @@
# Реальные выгрузки обработок (примеры, не для версионирования)
upload/
# Результаты сборки
build/
base/
*.epf
*.log
# Временные файлы тестов
test-tmp/
# Локальные настройки Claude Code
.claude/settings.local.json
# Инструменты (portable Apache и т.д.)
tools/
# Отладка навыков (eval, trigger-test, run_loop результаты)
debug/
# Кэш тестов навыков
tests/skills/.cache/
# Python кэш
__pycache__/
# Локальный реестр баз данных 1С
.v8-project.json
# ...но в кейсах он — вход теста (настройка newObjectPosition), и эталон без него
# на чистом клоне не сойдётся с результатом прогона.
!tests/skills/cases/**/.v8-project.json
# web-test: Node.js зависимости и runtime-артефакты
.claude/skills/web-test/scripts/node_modules/
.claude/skills/web-test/.browser-session.json
# Маркер отработавшего prepare() в фикстуре _suite-root
tests/web-test/_suite-root/prepare-ran.txt
# Скриншоты и видео (артефакты тестирования web-test)
*.png
*.mp4
# Навыки, скопированные для других AI-платформ (генерируются scripts/switch.py)
.agents/skills/
.augment/
.cline/
.codex/
.cursor/
.gemini/
.github/skills/
.kilocode/
.kiro/
.opencode/
.roo/
.windsurf/
debug-templates.txt
@@ -24,7 +24,7 @@ allowed-tools:
| `NoValidate` | Пропустить авто-валидацию | | `NoValidate` | Пропустить авто-валидацию |
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-edit.ps1" -ConfigPath '<path>' -Operation modify-property -Value 'Version=1.0.0.1' python ".roo/skills/cf-edit/scripts/cf-edit.py" -ConfigPath '<path>' -Operation modify-property -Value 'Version=1.0.0.1'
``` ```
## Операции ## Операции
@@ -23,7 +23,7 @@ allowed-tools:
| `OutFile` | Записать результат в файл (UTF-8 BOM) | | `OutFile` | Записать результат в файл (UTF-8 BOM) |
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-info.ps1" -ConfigPath "<путь>" python ".roo/skills/cf-info/scripts/cf-info.py" -ConfigPath "<путь>"
``` ```
## Три режима ## Три режима
@@ -39,7 +39,7 @@ allowed-tools:
не будет. не будет.
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-init.ps1" -Name "МояКонфигурация" python ".roo/skills/cf-init/scripts/cf-init.py" -Name "МояКонфигурация"
``` ```
## Примеры ## Примеры
@@ -24,6 +24,6 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-validate.ps1" -ConfigPath "upload/cfempty" python ".roo/skills/cf-validate/scripts/cf-validate.py" -ConfigPath "upload/cfempty"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-validate.ps1" -ConfigPath "upload/cfempty/Configuration.xml" python ".roo/skills/cf-validate/scripts/cf-validate.py" -ConfigPath "upload/cfempty/Configuration.xml"
``` ```
@@ -71,7 +71,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-borrow.ps1" -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Контрагенты" python ".roo/skills/cfe-borrow/scripts/cfe-borrow.py" -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Контрагенты"
``` ```
## Примеры ## Примеры
@@ -23,7 +23,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-diff.ps1" -ExtensionPath src\cfe\extname -ConfigPath src\cf -Mode A python ".roo/skills/cfe-diff/scripts/cfe-diff.py" -ExtensionPath src\cfe\extname -ConfigPath src\cf -Mode A
``` ```
## Mode A — обзор расширения ## Mode A — обзор расширения
@@ -1,4 +1,4 @@
# cfe-diff v1.5 — Analyze and compare 1C configuration extension (CFE) # cfe-diff v1.6 — Analyze and compare 1C configuration extension (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
[CmdletBinding(PositionalBinding=$false)] [CmdletBinding(PositionalBinding=$false)]
param( param(
@@ -162,24 +162,92 @@ function Get-BslFiles {
return $bslFiles return $bslFiles
} }
# --- Helper: parse interceptors from .bsl --- # Built-in language keywords in both spellings. The platform accepts either one in any module
# (pairs taken from the platform string tables), so a module written in English is ordinary
# source, not a broken one: we must read both and emit the spelling we read.
function Get-BslKeywords {
return @{
ru = @{
Async="Асинх"; Proc="Процедура"; EndProc="КонецПроцедуры"
Func="Функция"; EndFunc="КонецФункции"; Val="Знач"
Region="Область"; EndRegion="КонецОбласти"
If="Если"; Then="Тогда"; ElsIf="ИначеЕсли"; Else="Иначе"; EndIf="КонецЕсли"
# Platform spells the negation "Не"; we emit "НЕ" as this skill always has —
# the language is case-insensitive, and the lower case would churn every snapshot.
And="И"; Or="Или"; Not="НЕ"
Insert="Вставка"; EndInsert="КонецВставки"; Delete="Удаление"; EndDelete="КонецУдаления"
Before="Перед"; After="После"; Around="Вместо"; Control="ИзменениеИКонтроль"
Proceed="ПродолжитьВызов"; Return="Возврат"
# Not a keyword: name of the local the generated Instead-stub declares. Lives here so
# that the language of emitted text is decided in exactly one place.
ResultVar="Результат"
Directives=@("НаКлиенте", "НаСервере", "НаСервереБезКонтекста", "НаКлиентеНаСервереБезКонтекста", "НаКлиентеНаСервере")
}
en = @{
Async="Async"; Proc="Procedure"; EndProc="EndProcedure"
Func="Function"; EndFunc="EndFunction"; Val="Val"
Region="Region"; EndRegion="EndRegion"
If="If"; Then="Then"; ElsIf="ElsIf"; Else="Else"; EndIf="EndIf"
And="And"; Or="Or"; Not="Not"
Insert="Insert"; EndInsert="EndInsert"; Delete="Delete"; EndDelete="EndDelete"
Before="Before"; After="After"; Around="Around"; Control="ChangeAndValidate"
Proceed="ProceedWithCall"; Return="Return"
ResultVar="Result"
Directives=@("AtClient", "AtServer", "AtServerNoContext", "AtClientAtServerNoContext", "AtClientAtServer")
}
}
}
$script:bslKw = Get-BslKeywords
# Edit marker kind at the start of a line: Insert/Delete/EndInsert/EndDelete, $null if none.
# The marker owns the start of the line only — a trailing comment does not bother the platform
# and must not bother us either.
function Get-BslMarkerKind {
param([string]$line)
$kw = $script:bslKw
foreach ($k in @("EndInsert", "EndDelete", "Insert", "Delete")) {
if ($line -match ('^\s*#(?:' + $kw.ru[$k] + '|' + $kw.en[$k] + ')\b')) { return $k }
}
return $null
}
# Parse interceptor annotations present in a module. Type is normalized to the Russian spelling
# so everything downstream stays single-language; Raw keeps what the file actually says.
function Get-Interceptors { function Get-Interceptors {
param($lines)
$kw = $script:bslKw
$keys = @("Before", "After", "Control", "Around")
$alt = @()
$norm = @{}
foreach ($k in $keys) {
$alt += $kw.ru[$k]; $alt += $kw.en[$k]
$norm[$kw.ru[$k].ToLower()] = $kw.ru[$k]
$norm[$kw.en[$k].ToLower()] = $kw.ru[$k]
}
$re = '^&(' + ($alt -join '|') + ')\("([^"]+)"\)'
$result = @()
for ($i = 0; $i -lt $lines.Count; $i++) {
$t = $lines[$i].Trim()
if ($t -match $re) {
$raw = $Matches[1]
$result += @{ Type = $norm[$raw.ToLower()]; Raw = $raw; Method = $Matches[2]; Line = $i }
}
}
return $result
}
# --- Helper: read a .bsl and parse its interceptors ---
function Get-FileInterceptors {
param([string]$bslPath) param([string]$bslPath)
if (-not (Test-Path $bslPath)) { return @() } if (-not (Test-Path $bslPath)) { return @() }
$lines = [System.IO.File]::ReadAllLines($bslPath, [System.Text.Encoding]::UTF8) $lines = [System.IO.File]::ReadAllLines($bslPath, [System.Text.Encoding]::UTF8)
$interceptors = @() $result = @()
$i = 0 foreach ($ic in (Get-Interceptors $lines)) {
while ($i -lt $lines.Count) { $result += @{ Type = $ic.Type; Raw = $ic.Raw; Method = $ic.Method; Line = $ic.Line + 1; File = $bslPath }
$line = $lines[$i].Trim()
if ($line -match '^&(Перед|После|ИзменениеИКонтроль|Вместо)\("([^"]+)"\)') {
$type = $Matches[1]
$method = $Matches[2]
$interceptors += @{ Type = $type; Method = $method; Line = $i + 1; File = $bslPath }
} }
$i++ return $result
}
return $interceptors
} }
# --- Helper: extract #Вставка blocks from .bsl --- # --- Helper: extract #Вставка blocks from .bsl ---
@@ -194,12 +262,13 @@ function Get-InsertionBlocks {
$startLine = 0 $startLine = 0
for ($i = 0; $i -lt $lines.Count; $i++) { for ($i = 0; $i -lt $lines.Count; $i++) {
$line = $lines[$i].Trim() $line = $lines[$i]
if ($line -eq "#Вставка") { $kind = Get-BslMarkerKind $line
if ($kind -eq "Insert") {
$inBlock = $true $inBlock = $true
$blockLines = @() $blockLines = @()
$startLine = $i + 1 $startLine = $i + 1
} elseif ($line -eq "#КонецВставки" -and $inBlock) { } elseif ($kind -eq "EndInsert" -and $inBlock) {
$inBlock = $false $inBlock = $false
$blocks += @{ $blocks += @{
StartLine = $startLine StartLine = $startLine
@@ -298,10 +367,10 @@ if ($Mode -eq "A") {
$bslFiles = Get-BslFiles $obj.Type $obj.Name $bslFiles = Get-BslFiles $obj.Type $obj.Name
foreach ($bsl in $bslFiles) { foreach ($bsl in $bslFiles) {
$relPath = $bsl.Replace($ExtensionPath, "").TrimStart("\", "/") $relPath = $bsl.Replace($ExtensionPath, "").TrimStart("\", "/")
$interceptors = Get-Interceptors $bsl $interceptors = Get-FileInterceptors $bsl
if ($interceptors.Count -gt 0) { if ($interceptors.Count -gt 0) {
foreach ($ic in $interceptors) { foreach ($ic in $interceptors) {
Write-Host " &$($ic.Type)(`"$($ic.Method)`") — line $($ic.Line) in $relPath" Write-Host " &$($ic.Raw)(`"$($ic.Method)`") — line $($ic.Line) in $relPath"
} }
} else { } else {
Write-Host " $relPath (no interceptors)" Write-Host " $relPath (no interceptors)"
@@ -412,7 +481,7 @@ if ($Mode -eq "B") {
# Find .bsl files with &ИзменениеИКонтроль # Find .bsl files with &ИзменениеИКонтроль
$bslFiles = Get-BslFiles $obj.Type $obj.Name $bslFiles = Get-BslFiles $obj.Type $obj.Name
foreach ($bsl in $bslFiles) { foreach ($bsl in $bslFiles) {
$interceptors = Get-Interceptors $bsl $interceptors = Get-FileInterceptors $bsl
$macInterceptors = @($interceptors | Where-Object { $_.Type -eq "ИзменениеИКонтроль" }) $macInterceptors = @($interceptors | Where-Object { $_.Type -eq "ИзменениеИКонтроль" })
if ($macInterceptors.Count -eq 0) { continue } if ($macInterceptors.Count -eq 0) { continue }
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# cfe-diff v1.5 — Analyze and compare 1C configuration extension (CFE) # cfe-diff v1.6 — Analyze and compare 1C configuration extension (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -165,34 +165,97 @@ def get_bsl_files(obj_type, obj_name, extension_path):
return bsl_files return bsl_files
# --- Helper: parse interceptors from .bsl --- # Built-in language keywords in both spellings. The platform accepts either one in any module
# (pairs taken from the platform string tables), so a module written in English is ordinary
# source, not a broken one: we must read both and emit the spelling we read.
def bsl_keywords():
return {
"ru": {
"Async": "Асинх", "Proc": "Процедура", "EndProc": "КонецПроцедуры",
"Func": "Функция", "EndFunc": "КонецФункции", "Val": "Знач",
"Region": "Область", "EndRegion": "КонецОбласти",
"If": "Если", "Then": "Тогда", "ElsIf": "ИначеЕсли", "Else": "Иначе", "EndIf": "КонецЕсли",
# Platform spells the negation "Не"; we emit "НЕ" as this skill always has —
# the language is case-insensitive, and the lower case would churn every snapshot.
"And": "И", "Or": "Или", "Not": "НЕ",
"Insert": "Вставка", "EndInsert": "КонецВставки", "Delete": "Удаление", "EndDelete": "КонецУдаления",
"Before": "Перед", "After": "После", "Around": "Вместо", "Control": "ИзменениеИКонтроль",
"Proceed": "ПродолжитьВызов", "Return": "Возврат",
# Not a keyword: name of the local the generated Instead-stub declares. Lives here so
# that the language of emitted text is decided in exactly one place.
"ResultVar": "Результат",
"Directives": ["НаКлиенте", "НаСервере", "НаСервереБезКонтекста", "НаКлиентеНаСервереБезКонтекста", "НаКлиентеНаСервере"],
},
"en": {
"Async": "Async", "Proc": "Procedure", "EndProc": "EndProcedure",
"Func": "Function", "EndFunc": "EndFunction", "Val": "Val",
"Region": "Region", "EndRegion": "EndRegion",
"If": "If", "Then": "Then", "ElsIf": "ElsIf", "Else": "Else", "EndIf": "EndIf",
"And": "And", "Or": "Or", "Not": "Not",
"Insert": "Insert", "EndInsert": "EndInsert", "Delete": "Delete", "EndDelete": "EndDelete",
"Before": "Before", "After": "After", "Around": "Around", "Control": "ChangeAndValidate",
"Proceed": "ProceedWithCall", "Return": "Return",
"ResultVar": "Result",
"Directives": ["AtClient", "AtServer", "AtServerNoContext", "AtClientAtServerNoContext", "AtClientAtServer"],
},
}
def get_interceptors(bsl_path):
BSL_KW = bsl_keywords()
# Edit marker kind at the start of a line: Insert/Delete/EndInsert/EndDelete, None if none.
# The marker owns the start of the line only — a trailing comment does not bother the platform
# and must not bother us either.
def bsl_marker_kind(line):
kw = BSL_KW
for k in ("EndInsert", "EndDelete", "Insert", "Delete"):
if re.match(r'^\s*#(?:' + kw["ru"][k] + '|' + kw["en"][k] + r')\b', line, re.IGNORECASE):
return k
return None
# Parse interceptor annotations present in a module. Type is normalized to the Russian spelling
# so everything downstream stays single-language; raw keeps what the file actually says.
def get_interceptors(lines):
kw = BSL_KW
keys = ("Before", "After", "Control", "Around")
alt = []
norm = {}
for k in keys:
alt.append(kw["ru"][k])
alt.append(kw["en"][k])
norm[kw["ru"][k].lower()] = kw["ru"][k]
norm[kw["en"][k].lower()] = kw["ru"][k]
pat = re.compile(r'^&(' + '|'.join(alt) + r')\("([^"]+)"\)', re.IGNORECASE)
result = []
for i in range(len(lines)):
m = pat.match(lines[i].strip())
if m:
raw = m.group(1)
result.append({"type": norm[raw.lower()], "raw": raw, "method": m.group(2), "line": i})
return result
# --- Helper: read a .bsl and parse its interceptors ---
def get_file_interceptors(bsl_path):
if not os.path.isfile(bsl_path): if not os.path.isfile(bsl_path):
return [] return []
with open(bsl_path, "r", encoding="utf-8-sig") as fh: with open(bsl_path, "r", encoding="utf-8-sig") as fh:
lines = fh.readlines() lines = fh.readlines()
interceptors = [] return [{
pattern = re.compile(r'^&(\u041f\u0435\u0440\u0435\u0434|\u041f\u043e\u0441\u043b\u0435|\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435\u0418\u041a\u043e\u043d\u0442\u0440\u043e\u043b\u044c|\u0412\u043c\u0435\u0441\u0442\u043e)\("([^"]+)"\)') "Type": ic["type"],
# The above is: ^&(Перед|После|ИзменениеИКонтроль|Вместо)\("([^"]+)"\) "Raw": ic["raw"],
"Method": ic["method"],
for i, line in enumerate(lines): "Line": ic["line"] + 1,
stripped = line.strip()
m = pattern.match(stripped)
if m:
interceptors.append({
"Type": m.group(1),
"Method": m.group(2),
"Line": i + 1,
"File": bsl_path, "File": bsl_path,
}) } for ic in get_interceptors(lines)]
return interceptors
# --- Helper: extract #Вставка blocks from .bsl --- # --- Helper: extract insert-marker blocks from .bsl ---
def get_insertion_blocks(bsl_path): def get_insertion_blocks(bsl_path):
if not os.path.isfile(bsl_path): if not os.path.isfile(bsl_path):
@@ -207,14 +270,12 @@ def get_insertion_blocks(bsl_path):
start_line = 0 start_line = 0
for i, line in enumerate(lines): for i, line in enumerate(lines):
stripped = line.strip() kind = bsl_marker_kind(line)
if stripped == "\u0023\u0412\u0441\u0442\u0430\u0432\u043a\u0430": if kind == "Insert":
# #Вставка
in_block = True in_block = True
block_lines = [] block_lines = []
start_line = i + 1 start_line = i + 1
elif stripped == "\u0023\u041a\u043e\u043d\u0435\u0446\u0412\u0441\u0442\u0430\u0432\u043a\u0438" and in_block: elif kind == "EndInsert" and in_block:
# #КонецВставки
in_block = False in_block = False
blocks.append({ blocks.append({
"StartLine": start_line, "StartLine": start_line,
@@ -319,10 +380,10 @@ def mode_a(objects, extension_path):
bsl_files = get_bsl_files(obj["Type"], obj["Name"], extension_path) bsl_files = get_bsl_files(obj["Type"], obj["Name"], extension_path)
for bsl in bsl_files: for bsl in bsl_files:
rel_path = bsl.replace(extension_path, "").lstrip("\\/") rel_path = bsl.replace(extension_path, "").lstrip("\\/")
interceptor_list = get_interceptors(bsl) interceptor_list = get_file_interceptors(bsl)
if len(interceptor_list) > 0: if len(interceptor_list) > 0:
for ic in interceptor_list: for ic in interceptor_list:
print(f' &{ic["Type"]}("{ic["Method"]}") \u2014 line {ic["Line"]} in {rel_path}') print(f' &{ic["Raw"]}("{ic["Method"]}") \u2014 line {ic["Line"]} in {rel_path}')
else: else:
print(f" {rel_path} (no interceptors)") print(f" {rel_path} (no interceptors)")
@@ -434,7 +495,7 @@ def mode_b(objects, extension_path, config_path):
# Find .bsl files with &ИзменениеИКонтроль # Find .bsl files with &ИзменениеИКонтроль
bsl_files = get_bsl_files(obj["Type"], obj["Name"], extension_path) bsl_files = get_bsl_files(obj["Type"], obj["Name"], extension_path)
for bsl in bsl_files: for bsl in bsl_files:
interceptor_list = get_interceptors(bsl) interceptor_list = get_file_interceptors(bsl)
mac_interceptors = [ic for ic in interceptor_list if ic["Type"] == "\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435\u0418\u041a\u043e\u043d\u0442\u0440\u043e\u043b\u044c"] mac_interceptors = [ic for ic in interceptor_list if ic["Type"] == "\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435\u0418\u041a\u043e\u043d\u0442\u0440\u043e\u043b\u044c"]
if len(mac_interceptors) == 0: if len(mac_interceptors) == 0:
@@ -44,7 +44,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-init.ps1" -Name "МоёРасширение" -OutputDir "src\cfe\МоёРасширение" -ConfigPath "src\cf" python ".roo/skills/cfe-init/scripts/cfe-init.py" -Name "МоёРасширение" -OutputDir "src\cfe\МоёРасширение" -ConfigPath "src\cf"
``` ```
## Примеры ## Примеры
@@ -112,7 +112,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-patch-method.ps1" -ExtensionPath src\cfe\extname -ConfigPath src\cf -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before python ".roo/skills/cfe-patch-method/scripts/cfe-patch-method.py" -ExtensionPath src\cfe\extname -ConfigPath src\cf -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
``` ```
## Примеры ## Примеры
@@ -1,4 +1,4 @@
# cfe-patch-method v2.11 — Source-aware method interceptor for 1C extension (CFE) # cfe-patch-method v2.12 — Source-aware method interceptor for 1C extension (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
[CmdletBinding(PositionalBinding=$false)] [CmdletBinding(PositionalBinding=$false)]
param( param(
@@ -52,6 +52,105 @@ $script:decoratorMap = @{
"ModificationAndControl"="ИзменениеИКонтроль" "ModificationAndControl"="ИзменениеИКонтроль"
} }
# InterceptorType -> keyword table key (emission goes through the table, the map above stays
# the normalized value the logic compares against)
$script:decoratorKey = @{
"Before"="Before"; "After"="After"; "Instead"="Around"
"ModificationAndControl"="Control"
}
# Built-in language keywords in both spellings. The platform accepts either one in any module
# (pairs taken from the platform string tables), so a module written in English is ordinary
# source, not a broken one: we must read both and emit the spelling we read.
function Get-BslKeywords {
return @{
ru = @{
Async="Асинх"; Proc="Процедура"; EndProc="КонецПроцедуры"
Func="Функция"; EndFunc="КонецФункции"; Val="Знач"
Region="Область"; EndRegion="КонецОбласти"
If="Если"; Then="Тогда"; ElsIf="ИначеЕсли"; Else="Иначе"; EndIf="КонецЕсли"
# Platform spells the negation "Не"; we emit "НЕ" as this skill always has —
# the language is case-insensitive, and the lower case would churn every snapshot.
And="И"; Or="Или"; Not="НЕ"
Insert="Вставка"; EndInsert="КонецВставки"; Delete="Удаление"; EndDelete="КонецУдаления"
Before="Перед"; After="После"; Around="Вместо"; Control="ИзменениеИКонтроль"
Proceed="ПродолжитьВызов"; Return="Возврат"
# Not a keyword: name of the local the generated Instead-stub declares. Lives here so
# that the language of emitted text is decided in exactly one place.
ResultVar="Результат"
Directives=@("НаКлиенте", "НаСервере", "НаСервереБезКонтекста", "НаКлиентеНаСервереБезКонтекста", "НаКлиентеНаСервере")
}
en = @{
Async="Async"; Proc="Procedure"; EndProc="EndProcedure"
Func="Function"; EndFunc="EndFunction"; Val="Val"
Region="Region"; EndRegion="EndRegion"
If="If"; Then="Then"; ElsIf="ElsIf"; Else="Else"; EndIf="EndIf"
And="And"; Or="Or"; Not="Not"
Insert="Insert"; EndInsert="EndInsert"; Delete="Delete"; EndDelete="EndDelete"
Before="Before"; After="After"; Around="Around"; Control="ChangeAndValidate"
Proceed="ProceedWithCall"; Return="Return"
ResultVar="Result"
Directives=@("AtClient", "AtServer", "AtServerNoContext", "AtClientAtServerNoContext", "AtClientAtServer")
}
}
}
$script:bslKw = Get-BslKeywords
# Regex alternation for a keyword in both spellings
function Get-KwAlt {
param([string]$key)
return '(?:' + $script:bslKw.ru[$key] + '|' + $script:bslKw.en[$key] + ')'
}
# Spelling language of a recognized keyword. English is the discriminator: a Russian keyword
# never equals an English one, so anything else is 'ru'.
function Get-BslLang {
param([string]$word)
$w = $word.Trim().ToLower()
foreach ($k in @("Async", "Proc", "EndProc", "Func", "EndFunc", "Region", "If", "Insert", "Delete", "Before", "After", "Around", "Control")) {
if ($script:bslKw.en[$k].ToLower() -eq $w) { return "en" }
}
return "ru"
}
# Edit marker kind at the start of a line: Insert/Delete/EndInsert/EndDelete, $null if none.
# The marker owns the start of the line only — a trailing comment does not bother the platform
# and must not bother us either.
function Get-BslMarkerKind {
param([string]$line)
$kw = $script:bslKw
foreach ($k in @("EndInsert", "EndDelete", "Insert", "Delete")) {
if ($line -match ('^\s*#(?:' + $kw.ru[$k] + '|' + $kw.en[$k] + ')\b')) { return $k }
}
return $null
}
# Parse interceptor annotations present in a module. Type is normalized to the Russian spelling
# so everything downstream stays single-language; Raw keeps what the file actually says.
function Get-Interceptors {
param($lines)
$kw = $script:bslKw
$keys = @("Before", "After", "Control", "Around")
$alt = @()
$norm = @{}
foreach ($k in $keys) {
$alt += $kw.ru[$k]; $alt += $kw.en[$k]
$norm[$kw.ru[$k].ToLower()] = $kw.ru[$k]
$norm[$kw.en[$k].ToLower()] = $kw.ru[$k]
}
$re = '^&(' + ($alt -join '|') + ')\("([^"]+)"\)'
$result = @()
for ($i = 0; $i -lt $lines.Count; $i++) {
$t = $lines[$i].Trim()
if ($t -match $re) {
$raw = $Matches[1]
$result += @{ Type = $norm[$raw.ToLower()]; Raw = $raw; Method = $Matches[2]; Line = $i }
}
}
return $result
}
# Compute relative .bsl path segments from ModulePath (used for both ext and src) # Compute relative .bsl path segments from ModulePath (used for both ext and src)
function Get-ModuleRelPath { function Get-ModuleRelPath {
param([string]$modulePath) param([string]$modulePath)
@@ -125,7 +224,8 @@ function Split-TopLevel {
# Is a trimmed line a context-directive annotation? # Is a trimmed line a context-directive annotation?
function Test-ContextDirective { function Test-ContextDirective {
param([string]$trimmed) param([string]$trimmed)
return $trimmed -match '^&(НаКлиенте|НаСервере|НаСервереБезКонтекста|НаКлиентеНаСервереБезКонтекста|НаКлиентеНаСервере)\s*$' $alt = ($script:bslKw.ru.Directives + $script:bslKw.en.Directives) -join '|'
return $trimmed -match ('^&(' + $alt + ')\s*$')
} }
# Read a method signature starting at declaration line; returns @{ ParamsText; EndLineIdx } # Read a method signature starting at declaration line; returns @{ ParamsText; EndLineIdx }
@@ -163,46 +263,54 @@ function Read-Signature {
# Compute effective condition of the current branch of an #Если frame # Compute effective condition of the current branch of an #Если frame
function Get-EffectiveCondition { function Get-EffectiveCondition {
param($frame) param($frame, [string]$lang)
$kwNot = $script:bslKw[$lang].Not
$kwAnd = $script:bslKw[$lang].And
$conds = $frame.Conds $conds = $frame.Conds
$n = $conds.Count $n = $conds.Count
if ($frame.InElse) { if ($frame.InElse) {
$parts = @() $parts = @()
foreach ($c in $conds) { $parts += "НЕ ($c)" } foreach ($c in $conds) { $parts += "$kwNot ($c)" }
return ($parts -join " И ") return ($parts -join " $kwAnd ")
} }
if ($n -eq 1) { return $conds[0] } if ($n -eq 1) { return $conds[0] }
$parts = @() $parts = @()
for ($j = 0; $j -lt ($n - 1); $j++) { $parts += "НЕ ($($conds[$j]))" } for ($j = 0; $j -lt ($n - 1); $j++) { $parts += "$kwNot ($($conds[$j]))" }
$parts += $conds[$n - 1] $parts += $conds[$n - 1]
return ($parts -join " И ") return ($parts -join " $kwAnd ")
} }
# Extract the enclosing wrapper chain (regions + preprocessor) at a target line. # Extract the enclosing wrapper chain (regions + preprocessor) at a target line.
# Returns array outer->inner of @{ Kind='region'|'if'; Name=..; Cond=.. } # Returns array outer->inner of @{ Kind='region'|'if'; Name=..; Cond=.. }
function Get-EnclosingChain { function Get-EnclosingChain {
param($lines, [int]$targetIdx) param($lines, [int]$targetIdx, [string]$lang)
$reRegion = '^#' + (Get-KwAlt 'Region') + '\s+(\S+)'
$reEndRegion = '^#' + (Get-KwAlt 'EndRegion')
$reIf = '^#' + (Get-KwAlt 'If') + '\s+(.+?)\s+' + (Get-KwAlt 'Then')
$reElsIf = '^#' + (Get-KwAlt 'ElsIf') + '\s+(.+?)\s+' + (Get-KwAlt 'Then')
$reElse = '^#' + (Get-KwAlt 'Else') + '(\s|$)'
$reEndIf = '^#' + (Get-KwAlt 'EndIf')
$stack = New-Object System.Collections.ArrayList $stack = New-Object System.Collections.ArrayList
for ($i = 0; $i -lt $targetIdx; $i++) { for ($i = 0; $i -lt $targetIdx; $i++) {
$t = $lines[$i].Trim() $t = $lines[$i].Trim()
if ($t -match '^#Область\s+(\S+)') { if ($t -match $reRegion) {
[void]$stack.Add(@{ Kind = 'region'; Name = $Matches[1] }) [void]$stack.Add(@{ Kind = 'region'; Name = $Matches[1] })
} elseif ($t -match '^#КонецОбласти') { } elseif ($t -match $reEndRegion) {
for ($k = $stack.Count - 1; $k -ge 0; $k--) { if ($stack[$k].Kind -eq 'region') { $stack.RemoveAt($k); break } } for ($k = $stack.Count - 1; $k -ge 0; $k--) { if ($stack[$k].Kind -eq 'region') { $stack.RemoveAt($k); break } }
} elseif ($t -match '^#Если\s+(.+?)\s+Тогда') { } elseif ($t -match $reIf) {
[void]$stack.Add(@{ Kind = 'if'; Conds = @($Matches[1].Trim()); InElse = $false }) [void]$stack.Add(@{ Kind = 'if'; Conds = @($Matches[1].Trim()); InElse = $false })
} elseif ($t -match '^#ИначеЕсли\s+(.+?)\s+Тогда') { } elseif ($t -match $reElsIf) {
for ($k = $stack.Count - 1; $k -ge 0; $k--) { if ($stack[$k].Kind -eq 'if') { $stack[$k].Conds += $Matches[1].Trim(); $stack[$k].InElse = $false; break } } for ($k = $stack.Count - 1; $k -ge 0; $k--) { if ($stack[$k].Kind -eq 'if') { $stack[$k].Conds += $Matches[1].Trim(); $stack[$k].InElse = $false; break } }
} elseif ($t -match '^#Иначе(\s|$)') { } elseif ($t -match $reElse) {
for ($k = $stack.Count - 1; $k -ge 0; $k--) { if ($stack[$k].Kind -eq 'if') { $stack[$k].InElse = $true; break } } for ($k = $stack.Count - 1; $k -ge 0; $k--) { if ($stack[$k].Kind -eq 'if') { $stack[$k].InElse = $true; break } }
} elseif ($t -match '^#КонецЕсли') { } elseif ($t -match $reEndIf) {
for ($k = $stack.Count - 1; $k -ge 0; $k--) { if ($stack[$k].Kind -eq 'if') { $stack.RemoveAt($k); break } } for ($k = $stack.Count - 1; $k -ge 0; $k--) { if ($stack[$k].Kind -eq 'if') { $stack.RemoveAt($k); break } }
} }
} }
$chain = @() $chain = @()
foreach ($f in $stack) { foreach ($f in $stack) {
if ($f.Kind -eq 'region') { $chain += @{ Kind = 'region'; Name = $f.Name } } if ($f.Kind -eq 'region') { $chain += @{ Kind = 'region'; Name = $f.Name } }
else { $chain += @{ Kind = 'if'; Cond = (Get-EffectiveCondition $f) } } else { $chain += @{ Kind = 'if'; Cond = (Get-EffectiveCondition $f $lang) } }
} }
return ,$chain return ,$chain
} }
@@ -210,13 +318,14 @@ function Get-EnclosingChain {
# Extract a method from source .bsl lines. Returns $null if not found. # Extract a method from source .bsl lines. Returns $null if not found.
function Extract-Method { function Extract-Method {
param($lines, [string]$methodName) param($lines, [string]$methodName)
$declRe = '^\s*(Асинх\s+)?(Процедура|Функция)\s+(' + [regex]::Escape($methodName) + ')\s*\(' $declRe = '^\s*(' + (Get-KwAlt 'Async') + '\s+)?(' + (Get-KwAlt 'Proc') + '|' + (Get-KwAlt 'Func') + ')\s+(' + [regex]::Escape($methodName) + ')\s*\('
for ($i = 0; $i -lt $lines.Count; $i++) { for ($i = 0; $i -lt $lines.Count; $i++) {
if ($lines[$i] -imatch $declRe) { if ($lines[$i] -imatch $declRe) {
$isAsync = [bool]$Matches[1] $isAsync = [bool]$Matches[1]
$keyword = $Matches[2] $keyword = $Matches[2]
$canonical = $Matches[3] $canonical = $Matches[3]
$isFunction = ($keyword -ieq "Функция") $lang = Get-BslLang $keyword
$isFunction = ($keyword -ieq $script:bslKw.ru.Func -or $keyword -ieq $script:bslKw.en.Func)
$sig = Read-Signature $lines $i $sig = Read-Signature $lines $i
if (-not $sig) { throw "Не удалось разобрать сигнатуру метода '$methodName'" } if (-not $sig) { throw "Не удалось разобрать сигнатуру метода '$methodName'" }
@@ -227,13 +336,13 @@ function Extract-Method {
$paramNames = @() $paramNames = @()
if ($paramsText.Trim().Length -gt 0) { if ($paramsText.Trim().Length -gt 0) {
foreach ($seg in (Split-TopLevel $paramsText)) { foreach ($seg in (Split-TopLevel $paramsText)) {
$s = $seg.Trim() -replace '^Знач\s+', '' $s = $seg.Trim() -replace ('^' + (Get-KwAlt 'Val') + '\s+'), ''
if ($s -match '^([\w]+)') { $paramNames += $Matches[1] } if ($s -match '^([\w]+)') { $paramNames += $Matches[1] }
} }
} }
# Body: from sigEnd+1 to matching Конец* # Body: from sigEnd+1 to matching Конец*
$endRe = if ($isFunction) { '^\s*КонецФункции\b' } else { '^\s*КонецПроцедуры\b' } $endRe = if ($isFunction) { '^\s*' + (Get-KwAlt 'EndFunc') + '\b' } else { '^\s*' + (Get-KwAlt 'EndProc') + '\b' }
$bodyStart = $sigEnd + 1 $bodyStart = $sigEnd + 1
$bodyEnd = -1 $bodyEnd = -1
for ($j = $bodyStart; $j -lt $lines.Count; $j++) { for ($j = $bodyStart; $j -lt $lines.Count; $j++) {
@@ -251,10 +360,11 @@ function Extract-Method {
} }
# Enclosing chain (regions + preprocessor) # Enclosing chain (regions + preprocessor)
$chain = Get-EnclosingChain $lines $i $chain = Get-EnclosingChain $lines $i $lang
return @{ return @{
Canonical = $canonical Canonical = $canonical
Lang = $lang
IsFunction = $isFunction IsFunction = $isFunction
IsAsync = $isAsync IsAsync = $isAsync
ParamsText = $paramsText ParamsText = $paramsText
@@ -271,25 +381,13 @@ function Extract-Method {
return $null return $null
} }
# Parse interceptors present in a module (type + method)
function Get-Interceptors {
param($lines)
$result = @()
for ($i = 0; $i -lt $lines.Count; $i++) {
$t = $lines[$i].Trim()
if ($t -match '^&(Перед|После|ИзменениеИКонтроль|Вместо)\("([^"]+)"\)') {
$result += @{ Type = $Matches[1]; Method = $Matches[2]; Line = $i }
}
}
return $result
}
# Parse declared procedure/function names in a module # Parse declared procedure/function names in a module
function Get-ProcNames { function Get-ProcNames {
param($lines) param($lines)
$re = '^\s*(?:' + (Get-KwAlt 'Async') + '\s+)?(?:' + (Get-KwAlt 'Proc') + '|' + (Get-KwAlt 'Func') + ')\s+([\w]+)\s*\('
$names = @() $names = @()
foreach ($line in $lines) { foreach ($line in $lines) {
if ($line -imatch '^\s*(?:Асинх\s+)?(?:Процедура|Функция)\s+([\w]+)\s*\(') { $names += $Matches[1] } if ($line -imatch $re) { $names += $Matches[1] }
} }
return $names return $names
} }
@@ -300,14 +398,15 @@ function Get-ProcNames {
function Build-InterceptorCore { function Build-InterceptorCore {
param($method, [string]$interceptorType, [string]$interceptorName) param($method, [string]$interceptorType, [string]$interceptorName)
$decoratorRu = $script:decoratorMap[$interceptorType] $kw = $script:bslKw[$method.Lang]
$asyncPrefix = if ($method.IsAsync) { "Асинх " } else { "" } $decorator = $kw[$script:decoratorKey[$interceptorType]]
$keyword = if ($method.IsFunction) { "Функция" } else { "Процедура" } $asyncPrefix = if ($method.IsAsync) { "$($kw.Async) " } else { "" }
$endKeyword = if ($method.IsFunction) { "КонецФункции" } else { "КонецПроцедуры" } $keyword = if ($method.IsFunction) { $kw.Func } else { $kw.Proc }
$endKeyword = if ($method.IsFunction) { $kw.EndFunc } else { $kw.EndProc }
$lines = @() $lines = @()
if ($method.Context) { $lines += $method.Context } if ($method.Context) { $lines += $method.Context }
$lines += "&$decoratorRu(`"$($method.Canonical)`")" $lines += "&$decorator(`"$($method.Canonical)`")"
$lines += "$asyncPrefix$keyword $interceptorName($($method.ParamsText))" $lines += "$asyncPrefix$keyword $interceptorName($($method.ParamsText))"
switch ($interceptorType) { switch ($interceptorType) {
@@ -320,11 +419,11 @@ function Build-InterceptorCore {
"Instead" { "Instead" {
$namesJoined = ($method.ParamNames -join ", ") $namesJoined = ($method.ParamNames -join ", ")
if ($method.IsFunction) { if ($method.IsFunction) {
$lines += "`tРезультат = ПродолжитьВызов($namesJoined);" $lines += "`t$($kw.ResultVar) = $($kw.Proceed)($namesJoined);"
$lines += "`t// TODO: доработать поведение" $lines += "`t// TODO: доработать поведение"
$lines += "`tВозврат Результат;" $lines += "`t$($kw.Return) $($kw.ResultVar);"
} else { } else {
$lines += "`tПродолжитьВызов($namesJoined);" $lines += "`t$($kw.Proceed)($namesJoined);"
$lines += "`t// TODO: доработать поведение" $lines += "`t// TODO: доработать поведение"
} }
} }
@@ -339,16 +438,17 @@ function Build-InterceptorCore {
# Wrap core with region/preprocessor lines, adding blank lines ("air") around # Wrap core with region/preprocessor lines, adding blank lines ("air") around
# each structural boundary. Empty chain -> core as-is. # each structural boundary. Empty chain -> core as-is.
function Build-WrappedBlock { function Build-WrappedBlock {
param($chainArr, $core) param($chainArr, $core, [string]$lang)
$kw = $script:bslKw[$lang]
$b = @() $b = @()
foreach ($w in $chainArr) { foreach ($w in $chainArr) {
if ($w.Kind -eq 'region') { $b += "#Область $($w.Name)" } else { $b += "#Если $($w.Cond) Тогда" } if ($w.Kind -eq 'region') { $b += "#$($kw.Region) $($w.Name)" } else { $b += "#$($kw.If) $($w.Cond) $($kw.Then)" }
$b += "" $b += ""
} }
$b += $core $b += $core
for ($c = $chainArr.Count - 1; $c -ge 0; $c--) { for ($c = $chainArr.Count - 1; $c -ge 0; $c--) {
$b += "" $b += ""
if ($chainArr[$c].Kind -eq 'region') { $b += "#КонецОбласти" } else { $b += "#КонецЕсли" } if ($chainArr[$c].Kind -eq 'region') { $b += "#$($kw.EndRegion)" } else { $b += "#$($kw.EndIf)" }
} }
return $b return $b
} }
@@ -385,20 +485,27 @@ function Parse-MarkedBody {
$ops = @() # edit operations $ops = @() # edit operations
$i = 0 $i = 0
while ($i -lt $bodyLines.Count) { while ($i -lt $bodyLines.Count) {
$t = $bodyLines[$i].Trim() $kind = Get-BslMarkerKind $bodyLines[$i]
if ($t -eq '#Вставка') { if ($kind -eq 'Insert') {
# Marker lines are kept verbatim: they carry the author's spelling, indent and — since
# a tail comment is legal — the note explaining the edit. Re-emitting a bare keyword
# would silently drop that note on -Actualize.
$open = $bodyLines[$i]
$ins = @() $ins = @()
$i++ $i++
while ($i -lt $bodyLines.Count -and $bodyLines[$i].Trim() -ne '#КонецВставки') { $ins += $bodyLines[$i]; $i++ } while ($i -lt $bodyLines.Count -and (Get-BslMarkerKind $bodyLines[$i]) -ne 'EndInsert') { $ins += $bodyLines[$i]; $i++ }
$close = if ($i -lt $bodyLines.Count) { $bodyLines[$i] } else { $null }
$i++ # skip #КонецВставки $i++ # skip #КонецВставки
$ops += @{ Kind = 'insert'; After = ($v1.Count - 1); Lines = $ins } $ops += @{ Kind = 'insert'; After = ($v1.Count - 1); Lines = $ins; Open = $open; Close = $close }
} elseif ($t -eq '#Удаление') { } elseif ($kind -eq 'Delete') {
$open = $bodyLines[$i]
$startIdx = $v1.Count $startIdx = $v1.Count
$i++ $i++
$del = @() $del = @()
while ($i -lt $bodyLines.Count -and $bodyLines[$i].Trim() -ne '#КонецУдаления') { $del += $bodyLines[$i]; $v1 += $bodyLines[$i]; $i++ } while ($i -lt $bodyLines.Count -and (Get-BslMarkerKind $bodyLines[$i]) -ne 'EndDelete') { $del += $bodyLines[$i]; $v1 += $bodyLines[$i]; $i++ }
$close = if ($i -lt $bodyLines.Count) { $bodyLines[$i] } else { $null }
$i++ # skip #КонецУдаления $i++ # skip #КонецУдаления
$ops += @{ Kind = 'delete'; Start = $startIdx; End = ($v1.Count - 1); Lines = $del } $ops += @{ Kind = 'delete'; Start = $startIdx; End = ($v1.Count - 1); Lines = $del; Open = $open; Close = $close }
} else { } else {
$v1 += $bodyLines[$i] $v1 += $bodyLines[$i]
$i++ $i++
@@ -594,7 +701,8 @@ function Get-ResyncConflictReason {
# Write per-method conflict folder: conflict.md + base/local/remote # Write per-method conflict folder: conflict.md + base/local/remote
function Write-ConflictFolder { function Write-ConflictFolder {
param($folder, $methodId, $extBsl, $existingName, $method, $v1, $markedBody, $v2, $v1norm, $v2norm, $disputed, $enc) param($folder, $methodId, $extBsl, $existingName, $method, $v1, $markedBody, $v2, $v1norm, $v2norm, $disputed, [string]$lang, $enc)
$kw = $script:bslKw[$lang]
if (-not (Test-Path $folder)) { New-Item -ItemType Directory -Path $folder -Force | Out-Null } if (-not (Test-Path $folder)) { New-Item -ItemType Directory -Path $folder -Force | Out-Null }
[IO.File]::WriteAllText((Join-Path $folder 'base.bsl'), (($v1 -join "`r`n") + "`r`n"), $enc) [IO.File]::WriteAllText((Join-Path $folder 'base.bsl'), (($v1 -join "`r`n") + "`r`n"), $enc)
[IO.File]::WriteAllText((Join-Path $folder 'local.bsl'), (($markedBody -join "`r`n") + "`r`n"), $enc) [IO.File]::WriteAllText((Join-Path $folder 'local.bsl'), (($markedBody -join "`r`n") + "`r`n"), $enc)
@@ -602,7 +710,7 @@ function Write-ConflictFolder {
$md = @() $md = @()
$md += "# $methodId" $md += "# $methodId"
$md += "Править: $extBsl" $md += "Править: $extBsl"
$md += "Метод: $existingName (&ИзменениеИКонтроль(`"$($method.Canonical)`"))" $md += "Метод: $existingName (&$($kw.Control)(`"$($method.Canonical)`"))"
$md += "Причина: $(Get-ResyncConflictReason $disputed)" $md += "Причина: $(Get-ResyncConflictReason $disputed)"
$md += "" $md += ""
$md += "## Не размещено — перенести вручную" $md += "## Не размещено — перенести вручную"
@@ -614,10 +722,12 @@ function Write-ConflictFolder {
$md += "### Конфликт №$cn — вставка" $md += "### Конфликт №$cn — вставка"
$md += "Как блок стоял в вашей версии (local):" $md += "Как блок стоял в вашей версии (local):"
if ($d.Before -and $d.Before.Count -gt 0) { foreach ($l in $d.Before) { $md += $l } } if ($d.Before -and $d.Before.Count -gt 0) { foreach ($l in $d.Before) { $md += $l } }
$md += "#Вставка"; foreach ($l in $d.Lines) { $md += $l }; $md += "#КонецВставки" $openLine = if ([string]::IsNullOrEmpty($d.Open)) { "#$($kw.Insert)" } else { $d.Open }
$closeLine = if ([string]::IsNullOrEmpty($d.Close)) { "#$($kw.EndInsert)" } else { $d.Close }
$md += $openLine; foreach ($l in $d.Lines) { $md += $l }; $md += $closeLine
if ($d.After -and $d.After.Count -gt 0) { foreach ($l in $d.After) { $md += $l } } if ($d.After -and $d.After.Count -gt 0) { foreach ($l in $d.After) { $md += $l } }
$md += "" $md += ""
$md += "Якорь (строки вокруг #Вставка) изменился/исчез в новом оригинале — блок не лёг автоматически (см. дифф base→remote ниже)." $md += "Якорь (строки вокруг #$($kw.Insert)) изменился/исчез в новом оригинале — блок не лёг автоматически (см. дифф base→remote ниже)."
$md += "В модуле расширения блок припаркован в конце метода под меткой // [РЕСИНК-КОНФЛИКТ №$cn] — найди по ней." $md += "В модуле расширения блок припаркован в конце метода под меткой // [РЕСИНК-КОНФЛИКТ №$cn] — найди по ней."
$md += "Куда переносить: если якорного кода в новом методе больше нет — он, вероятно, вынесен/отрефакторен (ищите в диффе новый вызов/процедуру). Размести адаптацию по смыслу: например пост-обработкой после нового вызова, либо в заимствованной процедуре, куда переехал код. При правке файла сохрани кодировку (UTF-8 с BOM)." $md += "Куда переносить: если якорного кода в новом методе больше нет — он, вероятно, вынесен/отрефакторен (ищите в диффе новый вызов/процедуру). Размести адаптацию по смыслу: например пост-обработкой после нового вызова, либо в заимствованной процедуре, куда переехал код. При правке файла сохрани кодировку (UTF-8 с BOM)."
} else { } else {
@@ -644,15 +754,20 @@ function Invoke-Resync {
$methodId = "$logicalModule.$($method.Canonical)" $methodId = "$logicalModule.$($method.Canonical)"
$decLine = $dup.Line $decLine = $dup.Line
$sigLineIdx = $decLine + 1 $sigLineIdx = $decLine + 1
if ($sigLineIdx -ge $extLines.Count -or $extLines[$sigLineIdx] -notmatch '^\s*(?:Асинх\s+)?(?:Процедура|Функция)\s+([\w]+)\s*\(') { $declRe = '^\s*(?:' + (Get-KwAlt 'Async') + '\s+)?(' + (Get-KwAlt 'Proc') + '|' + (Get-KwAlt 'Func') + ')\s+([\w]+)\s*\('
if ($sigLineIdx -ge $extLines.Count -or $extLines[$sigLineIdx] -notmatch $declRe) {
return @{ Id = $methodId; Status = 'ОШИБКА'; ExtBsl = $extBsl; Reason = 'не разобрать сигнатуру перехватчика' } return @{ Id = $methodId; Status = 'ОШИБКА'; ExtBsl = $extBsl; Reason = 'не разобрать сигнатуру перехватчика' }
} }
$existingName = $Matches[1] # The block we are about to rewrite is the interceptor already in the extension, so it is
# its spelling — not the source module's — that the rewritten block must keep.
$lang = Get-BslLang $Matches[1]
$kw = $script:bslKw[$lang]
$existingName = $Matches[2]
$sig = Read-Signature $extLines $sigLineIdx $sig = Read-Signature $extLines $sigLineIdx
if (-not $sig) { return @{ Id = $methodId; Status = 'ОШИБКА'; ExtBsl = $extBsl; Reason = 'не разобрать сигнатуру' } } if (-not $sig) { return @{ Id = $methodId; Status = 'ОШИБКА'; ExtBsl = $extBsl; Reason = 'не разобрать сигнатуру' } }
$sigEnd = $sig.EndLineIdx $sigEnd = $sig.EndLineIdx
$isFunc = ($extLines[$sigLineIdx] -imatch '^\s*(?:Асинх\s+)?Функция\b') $isFunc = ($extLines[$sigLineIdx] -imatch ('^\s*(?:' + (Get-KwAlt 'Async') + '\s+)?' + (Get-KwAlt 'Func') + '\b'))
$endRe = if ($isFunc) { '^\s*КонецФункции\b' } else { '^\s*КонецПроцедуры\b' } $endRe = if ($isFunc) { '^\s*' + (Get-KwAlt 'EndFunc') + '\b' } else { '^\s*' + (Get-KwAlt 'EndProc') + '\b' }
$blockEnd = -1 $blockEnd = -1
for ($j = $sigEnd + 1; $j -lt $extLines.Count; $j++) { if ($extLines[$j] -imatch $endRe) { $blockEnd = $j; break } } for ($j = $sigEnd + 1; $j -lt $extLines.Count; $j++) { if ($extLines[$j] -imatch $endRe) { $blockEnd = $j; break } }
if ($blockEnd -lt 0) { return @{ Id = $methodId; Status = 'ОШИБКА'; ExtBsl = $extBsl; Reason = 'не найден конец перехватчика' } } if ($blockEnd -lt 0) { return @{ Id = $methodId; Status = 'ОШИБКА'; ExtBsl = $extBsl; Reason = 'не найден конец перехватчика' } }
@@ -703,14 +818,14 @@ function Invoke-Resync {
elseif ($null -eq $k) { elseif ($null -eq $k) {
$dbefore = @(); if ($op.After -ge 0) { $bz = [Math]::Max(0, $op.After - 2); for ($z = $bz; $z -le $op.After; $z++) { $dbefore += $v1[$z] } } $dbefore = @(); if ($op.After -ge 0) { $bz = [Math]::Max(0, $op.After - 2); for ($z = $bz; $z -le $op.After; $z++) { $dbefore += $v1[$z] } }
$dafter = @(); $az = [Math]::Min($v1.Count - 1, $op.After + 3); for ($z = $op.After + 1; $z -le $az; $z++) { $dafter += $v1[$z] } $dafter = @(); $az = [Math]::Min($v1.Count - 1, $op.After + 3); for ($z = $op.After + 1; $z -le $az; $z++) { $dafter += $v1[$z] }
$disputed += @{ Kind = 'insert'; Lines = $op.Lines; Before = $dbefore; After = $dafter } $disputed += @{ Kind = 'insert'; Lines = $op.Lines; Open = $op.Open; Close = $op.Close; Before = $dbefore; After = $dafter }
} }
elseif ($k -lt 0) { $insertTop += ,$op.Lines; $transferred += @{ Kind = 'insert' } } elseif ($k -lt 0) { $insertTop += $op; $transferred += @{ Kind = 'insert' } }
else { if (-not $insertAfter.ContainsKey($k)) { $insertAfter[$k] = @() }; $insertAfter[$k] += ,$op.Lines; $transferred += @{ Kind = 'insert' } } else { if (-not $insertAfter.ContainsKey($k)) { $insertAfter[$k] = @() }; $insertAfter[$k] += $op; $transferred += @{ Kind = 'insert' } }
} else { } else {
$keys = @(); for ($m = $op.Start; $m -le $op.End; $m++) { $keys += $v1norm[$m] } $keys = @(); for ($m = $op.Start; $m -le $op.End; $m++) { $keys += $v1norm[$m] }
$p = Find-UniqueRun $v2norm $keys $p = Find-UniqueRun $v2norm $keys
if ($p -ge 0) { $delStart[$p] = $true; $delEnd[$p + $keys.Count - 1] = $true; $transferred += @{ Kind = 'delete' } } if ($p -ge 0) { $delStart[$p] = $op.Open; $delEnd[$p + $keys.Count - 1] = $op.Close; $transferred += @{ Kind = 'delete' } }
else { else {
# Nearest significant neighbours around the deleted block; adjacency in the significant # Nearest significant neighbours around the deleted block; adjacency in the significant
# projection means the block is already cut (blanks/comments left behind don't matter). # projection means the block is already cut (blanks/comments left behind don't matter).
@@ -730,14 +845,16 @@ function Invoke-Resync {
return @{ Id = $methodId; Status = $st; ExtBsl = $extBsl; Transferred = $transferred.Count; Absorbed = $absorbed.Count; Disputed = $disputed.Count; Reason = $rsn; AbsorbedNotes = $absorbedNotes } return @{ Id = $methodId; Status = $st; ExtBsl = $extBsl; Transferred = $transferred.Count; Absorbed = $absorbed.Count; Disputed = $disputed.Count; Reason = $rsn; AbsorbedNotes = $absorbedNotes }
} }
# assemble new marked body # assemble new marked body. Marker lines come back as they were read (Open/Close); the keyword
# form is only a fallback for a block whose closing marker the module never had.
$mk = { param($orig, [string]$key) if ([string]::IsNullOrEmpty($orig)) { "#$($kw[$key])" } else { $orig } }
$newBody = @() $newBody = @()
foreach ($blk in $insertTop) { $newBody += "#Вставка"; foreach ($l in $blk) { $newBody += $l }; $newBody += "#КонецВставки" } foreach ($blk in $insertTop) { $newBody += (& $mk $blk.Open 'Insert'); foreach ($l in $blk.Lines) { $newBody += $l }; $newBody += (& $mk $blk.Close 'EndInsert') }
for ($k = 0; $k -lt $v2.Count; $k++) { for ($k = 0; $k -lt $v2.Count; $k++) {
if ($delStart.ContainsKey($k)) { $newBody += "#Удаление" } if ($delStart.ContainsKey($k)) { $newBody += (& $mk $delStart[$k] 'Delete') }
$newBody += $v2[$k] $newBody += $v2[$k]
if ($delEnd.ContainsKey($k)) { $newBody += "#КонецУдаления" } if ($delEnd.ContainsKey($k)) { $newBody += (& $mk $delEnd[$k] 'EndDelete') }
if ($insertAfter.ContainsKey($k)) { foreach ($blk in $insertAfter[$k]) { $newBody += "#Вставка"; foreach ($l in $blk) { $newBody += $l }; $newBody += "#КонецВставки" } } if ($insertAfter.ContainsKey($k)) { foreach ($blk in $insertAfter[$k]) { $newBody += (& $mk $blk.Open 'Insert'); foreach ($l in $blk.Lines) { $newBody += $l }; $newBody += (& $mk $blk.Close 'EndInsert') } }
} }
if ($disputed.Count -gt 0) { if ($disputed.Count -gt 0) {
$newBody += "`t// [РЕСИНК-КОНФЛИКТ] блоки ниже не легли автоматически — перенесите вручную (по № см. conflict.md / index.md в merge-воркспейсе, путь в выводе)." $newBody += "`t// [РЕСИНК-КОНФЛИКТ] блоки ниже не легли автоматически — перенесите вручную (по № см. conflict.md / index.md в merge-воркспейсе, путь в выводе)."
@@ -746,7 +863,7 @@ function Invoke-Resync {
$cn++ $cn++
if ($d.Kind -eq 'insert') { if ($d.Kind -eq 'insert') {
$newBody += "`t// [РЕСИНК-КОНФЛИКТ №$cn] вставка — исходный якорь изменён в новом оригинале." $newBody += "`t// [РЕСИНК-КОНФЛИКТ №$cn] вставка — исходный якорь изменён в новом оригинале."
$newBody += "#Вставка"; foreach ($l in $d.Lines) { $newBody += $l }; $newBody += "#КонецВставки" $newBody += (& $mk $d.Open 'Insert'); foreach ($l in $d.Lines) { $newBody += $l }; $newBody += (& $mk $d.Close 'EndInsert')
} }
else { else {
$newBody += "`t// [РЕСИНК-КОНФЛИКТ №$cn] удаление — строки не найдены в новом оригинале:" $newBody += "`t// [РЕСИНК-КОНФЛИКТ №$cn] удаление — строки не найдены в новом оригинале:"
@@ -754,12 +871,12 @@ function Invoke-Resync {
} }
} }
} }
$asyncPrefix = if ($method.IsAsync) { "Асинх " } else { "" } $asyncPrefix = if ($method.IsAsync) { "$($kw.Async) " } else { "" }
$keyword = if ($method.IsFunction) { "Функция" } else { "Процедура" } $keyword = if ($method.IsFunction) { $kw.Func } else { $kw.Proc }
$endKeyword = if ($method.IsFunction) { "КонецФункции" } else { "КонецПроцедуры" } $endKeyword = if ($method.IsFunction) { $kw.EndFunc } else { $kw.EndProc }
$newBlock = @() $newBlock = @()
if ($method.Context) { $newBlock += $method.Context } if ($method.Context) { $newBlock += $method.Context }
$newBlock += "&ИзменениеИКонтроль(`"$($method.Canonical)`")" $newBlock += "&$($kw.Control)(`"$($method.Canonical)`")"
$newBlock += "$asyncPrefix$keyword $existingName($($method.ParamsText))" $newBlock += "$asyncPrefix$keyword $existingName($($method.ParamsText))"
$newBlock += $newBody $newBlock += $newBody
$newBlock += $endKeyword $newBlock += $endKeyword
@@ -775,7 +892,7 @@ function Invoke-Resync {
$conflictDir = $null $conflictDir = $null
if ($disputed.Count -gt 0) { if ($disputed.Count -gt 0) {
$conflictDir = $conflictFolder $conflictDir = $conflictFolder
Write-ConflictFolder $conflictFolder $methodId $extBsl $existingName $method $v1 $markedBody $v2 $v1norm $v2norm $disputed $enc Write-ConflictFolder $conflictFolder $methodId $extBsl $existingName $method $v1 $markedBody $v2 $v1norm $v2norm $disputed $lang $enc
} }
$status = if ($disputed.Count -gt 0) { 'ЧАСТИЧНО' } elseif ($transferred.Count -eq 0 -and $absorbed.Count -gt 0) { 'ПЕРЕНЕСЕНО В ОСНОВНУЮ' } else { 'АКТУАЛИЗИРОВАН' } $status = if ($disputed.Count -gt 0) { 'ЧАСТИЧНО' } elseif ($transferred.Count -eq 0 -and $absorbed.Count -gt 0) { 'ПЕРЕНЕСЕНО В ОСНОВНУЮ' } else { 'АКТУАЛИЗИРОВАН' }
$rsn = if ($disputed.Count -gt 0) { Get-ResyncConflictReason $disputed } elseif ($status -eq 'ПЕРЕНЕСЕНО В ОСНОВНУЮ') { 'все правки уже в основной конфигурации — перехватчик можно удалить' } else { '' } $rsn = if ($disputed.Count -gt 0) { Get-ResyncConflictReason $disputed } elseif ($status -eq 'ПЕРЕНЕСЕНО В ОСНОВНУЮ') { 'все правки уже в основной конфигурации — перехватчик можно удалить' } else { '' }
@@ -1062,6 +1179,8 @@ if ($method.IsFunction -and ($InterceptorType -eq "Before" -or $InterceptorType
} }
$decoratorRu = $script:decoratorMap[$InterceptorType] $decoratorRu = $script:decoratorMap[$InterceptorType]
# What the annotation will actually look like in the module: the source's spelling
$decoratorOut = $script:bslKw[$method.Lang][$script:decoratorKey[$InterceptorType]]
# --- Read existing extension module (if any) --- # --- Read existing extension module (if any) ---
$extLines = @() $extLines = @()
@@ -1078,7 +1197,7 @@ $enc = New-Object System.Text.UTF8Encoding($true)
if ($dup) { if ($dup) {
if ($InterceptorType -ne "ModificationAndControl") { if ($InterceptorType -ne "ModificationAndControl") {
Write-Host "[ПРОПУЩЕН] Перехватчик &$decoratorRu(`"$MethodName`") уже есть в модуле — дубль не создаётся." Write-Host "[ПРОПУЩЕН] Перехватчик &$($dup.Raw)(`"$MethodName`") уже есть в модуле — дубль не создаётся."
Write-Host " Файл: $extBsl" Write-Host " Файл: $extBsl"
exit 0 exit 0
} }
@@ -1123,11 +1242,7 @@ $candidate = "${namePrefix}$($method.Canonical)"
$taken = @($existingProcNames | ForEach-Object { $_.ToLower() }) $taken = @($existingProcNames | ForEach-Object { $_.ToLower() })
$interceptorName = $candidate $interceptorName = $candidate
if ($taken -contains $candidate.ToLower()) { if ($taken -contains $candidate.ToLower()) {
if ($InterceptorType -eq "ModificationAndControl") { $interceptorName = "${candidate}_$($script:bslKw[$method.Lang][$script:decoratorKey[$InterceptorType]])"
$interceptorName = "${candidate}_ИзменениеИКонтроль"
} else {
$interceptorName = "${candidate}_$decoratorRu"
}
} }
$core = Build-InterceptorCore $method $InterceptorType $interceptorName $core = Build-InterceptorCore $method $InterceptorType $interceptorName
@@ -1142,7 +1257,7 @@ if ($extExists) {
if ($chain[$c].Kind -eq 'region') { if ($chain[$c].Kind -eq 'region') {
$rname = $chain[$c].Name $rname = $chain[$c].Name
for ($li = 0; $li -lt $extLines.Count; $li++) { for ($li = 0; $li -lt $extLines.Count; $li++) {
if ($extLines[$li].Trim() -match ('^#Область\s+' + [regex]::Escape($rname) + '\s*$')) { if ($extLines[$li].Trim() -match ('^#' + (Get-KwAlt 'Region') + '\s+' + [regex]::Escape($rname) + '\s*$')) {
$reuseRegionIdx = $c; $reuseLineIdx = $li; break $reuseRegionIdx = $c; $reuseLineIdx = $li; break
} }
} }
@@ -1157,14 +1272,14 @@ if ($reuseRegionIdx -ge 0) {
$innerChain = @() $innerChain = @()
for ($c = $reuseRegionIdx + 1; $c -lt $chain.Count; $c++) { $innerChain += $chain[$c] } for ($c = $reuseRegionIdx + 1; $c -lt $chain.Count; $c++) { $innerChain += $chain[$c] }
$block = Build-WrappedBlock $innerChain $core $block = Build-WrappedBlock $innerChain $core $method.Lang
# find matching #КонецОбласти for the reused region # find matching #КонецОбласти for the reused region
$depth = 0; $closeIdx = -1 $depth = 0; $closeIdx = -1
for ($li = $reuseLineIdx; $li -lt $extLines.Count; $li++) { for ($li = $reuseLineIdx; $li -lt $extLines.Count; $li++) {
$t = $extLines[$li].Trim() $t = $extLines[$li].Trim()
if ($t -match '^#Область\s') { $depth++ } if ($t -match ('^#' + (Get-KwAlt 'Region') + '\s')) { $depth++ }
elseif ($t -match '^#КонецОбласти') { $depth--; if ($depth -eq 0) { $closeIdx = $li; break } } elseif ($t -match ('^#' + (Get-KwAlt 'EndRegion'))) { $depth--; if ($depth -eq 0) { $closeIdx = $li; break } }
} }
if ($closeIdx -lt 0) { Write-Error "Не найден #КонецОбласти для региона (переиспользование)"; exit 1 } if ($closeIdx -lt 0) { Write-Error "Не найден #КонецОбласти для региона (переиспользование)"; exit 1 }
@@ -1181,7 +1296,7 @@ if ($reuseRegionIdx -ge 0) {
$placement = "в существующий регион '$($chain[$reuseRegionIdx].Name)'" $placement = "в существующий регион '$($chain[$reuseRegionIdx].Name)'"
} else { } else {
# Build full wrapper chain (source order) and append (or create file). # Build full wrapper chain (source order) and append (or create file).
$block = Build-WrappedBlock $chain $core $block = Build-WrappedBlock $chain $core $method.Lang
$blockText = ($block -join "`r`n") + "`r`n" $blockText = ($block -join "`r`n") + "`r`n"
$bslDir = Split-Path $extBsl -Parent $bslDir = Split-Path $extBsl -Parent
@@ -1210,7 +1325,7 @@ if ($flagTarget) {
Set-PropertyStateFlag $flagTarget.File $flagTarget.Property (Detect-FormatVersion $ExtensionPath) Set-PropertyStateFlag $flagTarget.File $flagTarget.Property (Detect-FormatVersion $ExtensionPath)
} }
Write-Host "[OK] Перехватчик &$decoratorRu(`"$MethodName`") — $placement" Write-Host "[OK] Перехватчик &$decoratorOut(`"$MethodName`") — $placement"
Write-Host " Файл: $extBsl" Write-Host " Файл: $extBsl"
Write-Host " Процедура: $interceptorName($($method.ParamsText -replace '\s+', ' '))" Write-Host " Процедура: $interceptorName($($method.ParamsText -replace '\s+', ' '))"
if ($method.Context) { Write-Host " Контекст: $($method.Context)" } if ($method.Context) { Write-Host " Контекст: $($method.Context)" }
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# cfe-patch-method v2.11 — Source-aware method interceptor for 1C extension (CFE) # cfe-patch-method v2.12 — Source-aware method interceptor for 1C extension (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -70,8 +70,83 @@ DECORATOR_MAP = {
"ModificationAndControl": "ИзменениеИКонтроль", "ModificationAndControl": "ИзменениеИКонтроль",
} }
# InterceptorType -> keyword table key (emission goes through the table, the map above stays
# the normalized value the logic compares against)
DECORATOR_KEY = {
"Before": "Before", "After": "After", "Instead": "Around",
"ModificationAndControl": "Control",
}
# Built-in language keywords in both spellings. The platform accepts either one in any module
# (pairs taken from the platform string tables), so a module written in English is ordinary
# source, not a broken one: we must read both and emit the spelling we read.
def bsl_keywords():
return {
"ru": {
"Async": "Асинх", "Proc": "Процедура", "EndProc": "КонецПроцедуры",
"Func": "Функция", "EndFunc": "КонецФункции", "Val": "Знач",
"Region": "Область", "EndRegion": "КонецОбласти",
"If": "Если", "Then": "Тогда", "ElsIf": "ИначеЕсли", "Else": "Иначе", "EndIf": "КонецЕсли",
# Platform spells the negation "Не"; we emit "НЕ" as this skill always has —
# the language is case-insensitive, and the lower case would churn every snapshot.
"And": "И", "Or": "Или", "Not": "НЕ",
"Insert": "Вставка", "EndInsert": "КонецВставки", "Delete": "Удаление", "EndDelete": "КонецУдаления",
"Before": "Перед", "After": "После", "Around": "Вместо", "Control": "ИзменениеИКонтроль",
"Proceed": "ПродолжитьВызов", "Return": "Возврат",
# Not a keyword: name of the local the generated Instead-stub declares. Lives here so
# that the language of emitted text is decided in exactly one place.
"ResultVar": "Результат",
"Directives": ["НаКлиенте", "НаСервере", "НаСервереБезКонтекста", "НаКлиентеНаСервереБезКонтекста", "НаКлиентеНаСервере"],
},
"en": {
"Async": "Async", "Proc": "Procedure", "EndProc": "EndProcedure",
"Func": "Function", "EndFunc": "EndFunction", "Val": "Val",
"Region": "Region", "EndRegion": "EndRegion",
"If": "If", "Then": "Then", "ElsIf": "ElsIf", "Else": "Else", "EndIf": "EndIf",
"And": "And", "Or": "Or", "Not": "Not",
"Insert": "Insert", "EndInsert": "EndInsert", "Delete": "Delete", "EndDelete": "EndDelete",
"Before": "Before", "After": "After", "Around": "Around", "Control": "ChangeAndValidate",
"Proceed": "ProceedWithCall", "Return": "Return",
"ResultVar": "Result",
"Directives": ["AtClient", "AtServer", "AtServerNoContext", "AtClientAtServerNoContext", "AtClientAtServer"],
},
}
BSL_KW = bsl_keywords()
# Regex alternation for a keyword in both spellings
def kw_alt(key):
return '(?:' + BSL_KW["ru"][key] + '|' + BSL_KW["en"][key] + ')'
# Spelling language of a recognized keyword. English is the discriminator: a Russian keyword
# never equals an English one, so anything else is 'ru'.
def bsl_lang(word):
w = word.strip().lower()
for k in ("Async", "Proc", "EndProc", "Func", "EndFunc", "Region", "If", "Insert", "Delete",
"Before", "After", "Around", "Control"):
if BSL_KW["en"][k].lower() == w:
return "en"
return "ru"
# Edit marker kind at the start of a line: Insert/Delete/EndInsert/EndDelete, None if none.
# The marker owns the start of the line only — a trailing comment does not bother the platform
# and must not bother us either.
def bsl_marker_kind(line):
kw = BSL_KW
for k in ("EndInsert", "EndDelete", "Insert", "Delete"):
if re.match(r'^\s*#(?:' + kw["ru"][k] + '|' + kw["en"][k] + r')\b', line, re.IGNORECASE):
return k
return None
CONTEXT_RE = re.compile( CONTEXT_RE = re.compile(
r'^&(НаКлиенте|НаСервере|НаСервереБезКонтекста|НаКлиентеНаСервереБезКонтекста|НаКлиентеНаСервере)\s*$' r'^&(' + '|'.join(BSL_KW["ru"]["Directives"] + BSL_KW["en"]["Directives"]) + r')\s*$',
re.IGNORECASE
) )
@@ -300,37 +375,45 @@ def read_signature(lines, start_idx):
return None return None
def effective_condition(frame): def effective_condition(frame, lang):
kw_not = BSL_KW[lang]["Not"]
kw_and = BSL_KW[lang]["And"]
conds = frame["conds"] conds = frame["conds"]
n = len(conds) n = len(conds)
if frame["in_else"]: if frame["in_else"]:
return " И ".join("НЕ (%s)" % c for c in conds) return (" %s " % kw_and).join("%s (%s)" % (kw_not, c) for c in conds)
if n == 1: if n == 1:
return conds[0] return conds[0]
parts = ["НЕ (%s)" % conds[j] for j in range(n - 1)] parts = ["%s (%s)" % (kw_not, conds[j]) for j in range(n - 1)]
parts.append(conds[n - 1]) parts.append(conds[n - 1])
return " И ".join(parts) return (" %s " % kw_and).join(parts)
def get_enclosing_chain(lines, target_idx): def get_enclosing_chain(lines, target_idx, lang):
re_region = re.compile(r'^#' + kw_alt("Region") + r'\s+(\S+)', re.IGNORECASE)
re_end_region = re.compile(r'^#' + kw_alt("EndRegion"), re.IGNORECASE)
re_if = re.compile(r'^#' + kw_alt("If") + r'\s+(.+?)\s+' + kw_alt("Then"), re.IGNORECASE)
re_elsif = re.compile(r'^#' + kw_alt("ElsIf") + r'\s+(.+?)\s+' + kw_alt("Then"), re.IGNORECASE)
re_else = re.compile(r'^#' + kw_alt("Else") + r'(\s|$)', re.IGNORECASE)
re_endif = re.compile(r'^#' + kw_alt("EndIf"), re.IGNORECASE)
stack = [] stack = []
for i in range(target_idx): for i in range(target_idx):
t = lines[i].strip() t = lines[i].strip()
m = re.match(r'^#Область\s+(\S+)', t) m = re_region.match(t)
if m: if m:
stack.append({"kind": "region", "name": m.group(1)}) stack.append({"kind": "region", "name": m.group(1)})
continue continue
if re.match(r'^#КонецОбласти', t): if re_end_region.match(t):
for k in range(len(stack) - 1, -1, -1): for k in range(len(stack) - 1, -1, -1):
if stack[k]["kind"] == "region": if stack[k]["kind"] == "region":
del stack[k] del stack[k]
break break
continue continue
m = re.match(r'^#Если\s+(.+?)\s+Тогда', t) m = re_if.match(t)
if m: if m:
stack.append({"kind": "if", "conds": [m.group(1).strip()], "in_else": False}) stack.append({"kind": "if", "conds": [m.group(1).strip()], "in_else": False})
continue continue
m = re.match(r'^#ИначеЕсли\s+(.+?)\s+Тогда', t) m = re_elsif.match(t)
if m: if m:
for k in range(len(stack) - 1, -1, -1): for k in range(len(stack) - 1, -1, -1):
if stack[k]["kind"] == "if": if stack[k]["kind"] == "if":
@@ -338,13 +421,13 @@ def get_enclosing_chain(lines, target_idx):
stack[k]["in_else"] = False stack[k]["in_else"] = False
break break
continue continue
if re.match(r'^#Иначе(\s|$)', t): if re_else.match(t):
for k in range(len(stack) - 1, -1, -1): for k in range(len(stack) - 1, -1, -1):
if stack[k]["kind"] == "if": if stack[k]["kind"] == "if":
stack[k]["in_else"] = True stack[k]["in_else"] = True
break break
continue continue
if re.match(r'^#КонецЕсли', t): if re_endif.match(t):
for k in range(len(stack) - 1, -1, -1): for k in range(len(stack) - 1, -1, -1):
if stack[k]["kind"] == "if": if stack[k]["kind"] == "if":
del stack[k] del stack[k]
@@ -355,13 +438,14 @@ def get_enclosing_chain(lines, target_idx):
if f["kind"] == "region": if f["kind"] == "region":
chain.append({"kind": "region", "name": f["name"]}) chain.append({"kind": "region", "name": f["name"]})
else: else:
chain.append({"kind": "if", "cond": effective_condition(f)}) chain.append({"kind": "if", "cond": effective_condition(f, lang)})
return chain return chain
def extract_method(lines, method_name): def extract_method(lines, method_name):
decl_re = re.compile( decl_re = re.compile(
r'^\s*(Асинх\s+)?(Процедура|Функция)\s+(' + re.escape(method_name) + r')\s*\(', r'^\s*(' + kw_alt("Async") + r'\s+)?(' + kw_alt("Proc") + '|' + kw_alt("Func") + r')\s+('
+ re.escape(method_name) + r')\s*\(',
re.IGNORECASE, re.IGNORECASE,
) )
for i in range(len(lines)): for i in range(len(lines)):
@@ -371,7 +455,8 @@ def extract_method(lines, method_name):
is_async = bool(m.group(1)) is_async = bool(m.group(1))
keyword = m.group(2) keyword = m.group(2)
canonical = m.group(3) canonical = m.group(3)
is_function = keyword.lower() == "функция" lang = bsl_lang(keyword)
is_function = keyword.lower() in (BSL_KW["ru"]["Func"].lower(), BSL_KW["en"]["Func"].lower())
sig = read_signature(lines, i) sig = read_signature(lines, i)
if not sig: if not sig:
@@ -381,12 +466,12 @@ def extract_method(lines, method_name):
param_names = [] param_names = []
if params_text.strip(): if params_text.strip():
for seg in split_top_level(params_text): for seg in split_top_level(params_text):
s = re.sub(r'^Знач\s+', '', seg.strip()) s = re.sub(r'^' + kw_alt("Val") + r'\s+', '', seg.strip(), flags=re.IGNORECASE)
mm = re.match(r'^([\w]+)', s) mm = re.match(r'^([\w]+)', s)
if mm: if mm:
param_names.append(mm.group(1)) param_names.append(mm.group(1))
end_re = re.compile(r'^\s*КонецФункции\b' if is_function else r'^\s*КонецПроцедуры\b', end_re = re.compile(r'^\s*' + (kw_alt("EndFunc") if is_function else kw_alt("EndProc")) + r'\b',
re.IGNORECASE) re.IGNORECASE)
body_start = sig_end + 1 body_start = sig_end + 1
body_end = -1 body_end = -1
@@ -404,10 +489,11 @@ def extract_method(lines, method_name):
if is_context_directive(prev): if is_context_directive(prev):
context = prev context = prev
chain = get_enclosing_chain(lines, i) chain = get_enclosing_chain(lines, i, lang)
return { return {
"canonical": canonical, "canonical": canonical,
"lang": lang,
"is_function": is_function, "is_function": is_function,
"is_async": is_async, "is_async": is_async,
"params_text": params_text, "params_text": params_text,
@@ -422,19 +508,32 @@ def extract_method(lines, method_name):
return None return None
# Parse interceptor annotations present in a module. Type is normalized to the Russian spelling
# so everything downstream stays single-language; raw keeps what the file actually says.
def get_interceptors(lines): def get_interceptors(lines):
kw = BSL_KW
keys = ("Before", "After", "Control", "Around")
alt = []
norm = {}
for k in keys:
alt.append(kw["ru"][k])
alt.append(kw["en"][k])
norm[kw["ru"][k].lower()] = kw["ru"][k]
norm[kw["en"][k].lower()] = kw["ru"][k]
pat = re.compile(r'^&(' + '|'.join(alt) + r')\("([^"]+)"\)', re.IGNORECASE)
result = [] result = []
pat = re.compile(r'^&(Перед|После|ИзменениеИКонтроль|Вместо)\("([^"]+)"\)')
for i in range(len(lines)): for i in range(len(lines)):
m = pat.match(lines[i].strip()) m = pat.match(lines[i].strip())
if m: if m:
result.append({"type": m.group(1), "method": m.group(2), "line": i}) raw = m.group(1)
result.append({"type": norm[raw.lower()], "raw": raw, "method": m.group(2), "line": i})
return result return result
def get_proc_names(lines): def get_proc_names(lines):
names = [] names = []
pat = re.compile(r'^\s*(?:Асинх\s+)?(?:Процедура|Функция)\s+([\w]+)\s*\(', re.IGNORECASE) pat = re.compile(r'^\s*(?:' + kw_alt("Async") + r'\s+)?(?:' + kw_alt("Proc") + '|' + kw_alt("Func")
+ r')\s+([\w]+)\s*\(', re.IGNORECASE)
for line in lines: for line in lines:
m = pat.match(line) m = pat.match(line)
if m: if m:
@@ -443,15 +542,16 @@ def get_proc_names(lines):
def build_interceptor_core(method, interceptor_type, interceptor_name): def build_interceptor_core(method, interceptor_type, interceptor_name):
decorator_ru = DECORATOR_MAP[interceptor_type] kw = BSL_KW[method["lang"]]
async_prefix = "Асинх " if method["is_async"] else "" decorator = kw[DECORATOR_KEY[interceptor_type]]
keyword = "Функция" if method["is_function"] else "Процедура" async_prefix = (kw["Async"] + " ") if method["is_async"] else ""
end_keyword = "КонецФункции" if method["is_function"] else "КонецПроцедуры" keyword = kw["Func"] if method["is_function"] else kw["Proc"]
end_keyword = kw["EndFunc"] if method["is_function"] else kw["EndProc"]
lines = [] lines = []
if method["context"]: if method["context"]:
lines.append(method["context"]) lines.append(method["context"])
lines.append('&%s("%s")' % (decorator_ru, method["canonical"])) lines.append('&%s("%s")' % (decorator, method["canonical"]))
lines.append("%s%s %s(%s)" % (async_prefix, keyword, interceptor_name, method["params_text"])) lines.append("%s%s %s(%s)" % (async_prefix, keyword, interceptor_name, method["params_text"]))
if interceptor_type == "Before": if interceptor_type == "Before":
@@ -461,11 +561,11 @@ def build_interceptor_core(method, interceptor_type, interceptor_name):
elif interceptor_type == "Instead": elif interceptor_type == "Instead":
names_joined = ", ".join(method["param_names"]) names_joined = ", ".join(method["param_names"])
if method["is_function"]: if method["is_function"]:
lines.append("\tРезультат = ПродолжитьВызов(%s);" % names_joined) lines.append("\t%s = %s(%s);" % (kw["ResultVar"], kw["Proceed"], names_joined))
lines.append("\t// TODO: доработать поведение") lines.append("\t// TODO: доработать поведение")
lines.append("\tВозврат Результат;") lines.append("\t%s %s;" % (kw["Return"], kw["ResultVar"]))
else: else:
lines.append("\tПродолжитьВызов(%s);" % names_joined) lines.append("\t%s(%s);" % (kw["Proceed"], names_joined))
lines.append("\t// TODO: доработать поведение") lines.append("\t// TODO: доработать поведение")
elif interceptor_type == "ModificationAndControl": elif interceptor_type == "ModificationAndControl":
lines.extend(method["body_lines"]) lines.extend(method["body_lines"])
@@ -474,16 +574,18 @@ def build_interceptor_core(method, interceptor_type, interceptor_name):
return lines return lines
def build_wrapped_block(chain_arr, core): def build_wrapped_block(chain_arr, core, lang):
"""Wrap core with region/preprocessor lines, adding blank lines around each boundary.""" """Wrap core with region/preprocessor lines, adding blank lines around each boundary."""
kw = BSL_KW[lang]
b = [] b = []
for w in chain_arr: for w in chain_arr:
b.append("#Область %s" % w["name"] if w["kind"] == "region" else "#Если %s Тогда" % w["cond"]) b.append(("#%s %s" % (kw["Region"], w["name"])) if w["kind"] == "region"
else ("#%s %s %s" % (kw["If"], w["cond"], kw["Then"])))
b.append("") b.append("")
b.extend(core) b.extend(core)
for w in reversed(chain_arr): for w in reversed(chain_arr):
b.append("") b.append("")
b.append("#КонецОбласти" if w["kind"] == "region" else "#КонецЕсли") b.append(("#" + kw["EndRegion"]) if w["kind"] == "region" else ("#" + kw["EndIf"]))
return b return b
@@ -512,25 +614,34 @@ def parse_marked_body(body_lines):
i = 0 i = 0
n = len(body_lines) n = len(body_lines)
while i < n: while i < n:
t = body_lines[i].strip() kind = bsl_marker_kind(body_lines[i])
if t == "#Вставка": if kind == "Insert":
# Marker lines are kept verbatim: they carry the author's spelling, indent and — since
# a tail comment is legal — the note explaining the edit. Re-emitting a bare keyword
# would silently drop that note on -Actualize.
open_line = body_lines[i]
ins = [] ins = []
i += 1 i += 1
while i < n and body_lines[i].strip() != "#КонецВставки": while i < n and bsl_marker_kind(body_lines[i]) != "EndInsert":
ins.append(body_lines[i]) ins.append(body_lines[i])
i += 1 i += 1
close_line = body_lines[i] if i < n else None
i += 1 i += 1
ops.append({"kind": "insert", "after": len(v1) - 1, "lines": ins}) ops.append({"kind": "insert", "after": len(v1) - 1, "lines": ins,
elif t == "#Удаление": "open": open_line, "close": close_line})
elif kind == "Delete":
open_line = body_lines[i]
start_idx = len(v1) start_idx = len(v1)
i += 1 i += 1
dels = [] dels = []
while i < n and body_lines[i].strip() != "#КонецУдаления": while i < n and bsl_marker_kind(body_lines[i]) != "EndDelete":
dels.append(body_lines[i]) dels.append(body_lines[i])
v1.append(body_lines[i]) v1.append(body_lines[i])
i += 1 i += 1
close_line = body_lines[i] if i < n else None
i += 1 i += 1
ops.append({"kind": "delete", "start": start_idx, "end": len(v1) - 1, "lines": dels}) ops.append({"kind": "delete", "start": start_idx, "end": len(v1) - 1, "lines": dels,
"open": open_line, "close": close_line})
else: else:
v1.append(body_lines[i]) v1.append(body_lines[i])
i += 1 i += 1
@@ -879,6 +990,8 @@ def main():
"(перехват &Перед/&После к функциям неприменим)." % method_name) "(перехват &Перед/&После к функциям неприменим)." % method_name)
decorator_ru = DECORATOR_MAP[interceptor_type] decorator_ru = DECORATOR_MAP[interceptor_type]
# What the annotation will actually look like in the module: the source's spelling
decorator_out = BSL_KW[method["lang"]][DECORATOR_KEY[interceptor_type]]
# --- Read existing extension module (if any) --- # --- Read existing extension module (if any) ---
ext_exists = os.path.isfile(ext_bsl) ext_exists = os.path.isfile(ext_bsl)
@@ -896,7 +1009,7 @@ def main():
if dup: if dup:
if interceptor_type != "ModificationAndControl": if interceptor_type != "ModificationAndControl":
print('[ПРОПУЩЕН] Перехватчик &%s("%s") уже есть в модуле — дубль не создаётся.' print('[ПРОПУЩЕН] Перехватчик &%s("%s") уже есть в модуле — дубль не создаётся.'
% (decorator_ru, method_name)) % (dup["raw"], method_name))
print(" Файл: %s" % ext_bsl) print(" Файл: %s" % ext_bsl)
sys.exit(0) sys.exit(0)
rel = rel_parts_under(extension_path, ext_bsl) rel = rel_parts_under(extension_path, ext_bsl)
@@ -940,14 +1053,11 @@ def main():
taken = [n.lower() for n in existing_proc_names] taken = [n.lower() for n in existing_proc_names]
interceptor_name = candidate interceptor_name = candidate
if candidate.lower() in taken: if candidate.lower() in taken:
if interceptor_type == "ModificationAndControl": interceptor_name = candidate + "_" + decorator_out
interceptor_name = candidate + "_ИзменениеИКонтроль"
else:
interceptor_name = candidate + "_" + decorator_ru
core = build_interceptor_core(method, interceptor_type, interceptor_name) core = build_interceptor_core(method, interceptor_type, interceptor_name)
place_new(ext_bsl, ext_lines, ext_exists, method["chain"], core) place_new(ext_bsl, ext_lines, ext_exists, method["chain"], core, method["lang"])
# Модуль в расширении есть — отражаем это в метаданных объекта (формат ≥ 2.19). # Модуль в расширении есть — отражаем это в метаданных объекта (формат ≥ 2.19).
flag_target = get_module_flag_target(rel_parts, extension_path) flag_target = get_module_flag_target(rel_parts, extension_path)
@@ -957,7 +1067,7 @@ def main():
# emit summary # emit summary
placement = place_new.placement placement = place_new.placement
print('[OK] Перехватчик &%s("%s") — %s' % (decorator_ru, method_name, placement)) print('[OK] Перехватчик &%s("%s") — %s' % (decorator_out, method_name, placement))
print(" Файл: %s" % ext_bsl) print(" Файл: %s" % ext_bsl)
print(" Процедура: %s(%s)" % (interceptor_name, normalize(method["params_text"]))) print(" Процедура: %s(%s)" % (interceptor_name, normalize(method["params_text"])))
if method["context"]: if method["context"]:
@@ -970,7 +1080,7 @@ def main():
print(" Обрамление: %s" % desc) print(" Обрамление: %s" % desc)
def place_new(ext_bsl, ext_lines, ext_exists, chain, core): def place_new(ext_bsl, ext_lines, ext_exists, chain, core, lang):
enc_bom = True # noqa enc_bom = True # noqa
# find innermost region in chain that already exists in the extension module # find innermost region in chain that already exists in the extension module
@@ -980,7 +1090,7 @@ def place_new(ext_bsl, ext_lines, ext_exists, chain, core):
for c in range(len(chain) - 1, -1, -1): for c in range(len(chain) - 1, -1, -1):
if chain[c]["kind"] == "region": if chain[c]["kind"] == "region":
rname = chain[c]["name"] rname = chain[c]["name"]
rre = re.compile(r'^#Область\s+' + re.escape(rname) + r'\s*$') rre = re.compile(r'^#' + kw_alt("Region") + r'\s+' + re.escape(rname) + r'\s*$', re.IGNORECASE)
for li in range(len(ext_lines)): for li in range(len(ext_lines)):
if rre.match(ext_lines[li].strip()): if rre.match(ext_lines[li].strip()):
reuse_region_idx = c reuse_region_idx = c
@@ -991,15 +1101,15 @@ def place_new(ext_bsl, ext_lines, ext_exists, chain, core):
if reuse_region_idx >= 0: if reuse_region_idx >= 0:
inner_chain = chain[reuse_region_idx + 1:] inner_chain = chain[reuse_region_idx + 1:]
block = build_wrapped_block(inner_chain, core) block = build_wrapped_block(inner_chain, core, lang)
depth = 0 depth = 0
close_idx = -1 close_idx = -1
for li in range(reuse_line_idx, len(ext_lines)): for li in range(reuse_line_idx, len(ext_lines)):
t = ext_lines[li].strip() t = ext_lines[li].strip()
if re.match(r'^#Область\s', t): if re.match(r'^#' + kw_alt("Region") + r'\s', t, re.IGNORECASE):
depth += 1 depth += 1
elif re.match(r'^#КонецОбласти', t): elif re.match(r'^#' + kw_alt("EndRegion"), t, re.IGNORECASE):
depth -= 1 depth -= 1
if depth == 0: if depth == 0:
close_idx = li close_idx = li
@@ -1021,7 +1131,7 @@ def place_new(ext_bsl, ext_lines, ext_exists, chain, core):
return return
# full wrapper chain, append (or create) # full wrapper chain, append (or create)
block = build_wrapped_block(chain, core) block = build_wrapped_block(chain, core, lang)
block_text = "\r\n".join(block) + "\r\n" block_text = "\r\n".join(block) + "\r\n"
bsl_dir = os.path.dirname(ext_bsl) bsl_dir = os.path.dirname(ext_bsl)
@@ -1083,7 +1193,8 @@ def conflict_reason(disputed):
return "; ".join(parts) return "; ".join(parts)
def write_conflict_folder(folder, method_id, ext_bsl, existing_name, method, v1, marked_body, v2, v1norm, v2norm, disputed): def write_conflict_folder(folder, method_id, ext_bsl, existing_name, method, v1, marked_body, v2, v1norm, v2norm, disputed, lang):
kw = BSL_KW[lang]
os.makedirs(folder, exist_ok=True) os.makedirs(folder, exist_ok=True)
write_bsl(os.path.join(folder, "base.bsl"), v1) write_bsl(os.path.join(folder, "base.bsl"), v1)
write_bsl(os.path.join(folder, "local.bsl"), marked_body) write_bsl(os.path.join(folder, "local.bsl"), marked_body)
@@ -1091,7 +1202,7 @@ def write_conflict_folder(folder, method_id, ext_bsl, existing_name, method, v1,
md = [] md = []
md.append("# %s" % method_id) md.append("# %s" % method_id)
md.append("Править: %s" % ext_bsl) md.append("Править: %s" % ext_bsl)
md.append('Метод: %s (&ИзменениеИКонтроль("%s"))' % (existing_name, method["canonical"])) md.append('Метод: %s (&%s("%s"))' % (existing_name, kw["Control"], method["canonical"]))
md.append("Причина: %s" % conflict_reason(disputed)) md.append("Причина: %s" % conflict_reason(disputed))
md.append("") md.append("")
md.append("## Не размещено — перенести вручную") md.append("## Не размещено — перенести вручную")
@@ -1105,12 +1216,13 @@ def write_conflict_folder(folder, method_id, ext_bsl, existing_name, method, v1,
if d.get("before"): if d.get("before"):
for l in d["before"]: for l in d["before"]:
md.append(l) md.append(l)
md.append("#Вставка"); md.extend(d["lines"]); md.append("#КонецВставки") md.append(d.get("open") or ("#" + kw["Insert"])); md.extend(d["lines"])
md.append(d.get("close") or ("#" + kw["EndInsert"]))
if d.get("after"): if d.get("after"):
for l in d["after"]: for l in d["after"]:
md.append(l) md.append(l)
md.append("") md.append("")
md.append("Якорь (строки вокруг #Вставка) изменился/исчез в новом оригинале — блок не лёг автоматически (см. дифф base→remote ниже).") md.append("Якорь (строки вокруг #%s) изменился/исчез в новом оригинале — блок не лёг автоматически (см. дифф base→remote ниже)." % kw["Insert"])
md.append("В модуле расширения блок припаркован в конце метода под меткой // [РЕСИНК-КОНФЛИКТ №%d] — найди по ней." % cn) md.append("В модуле расширения блок припаркован в конце метода под меткой // [РЕСИНК-КОНФЛИКТ №%d] — найди по ней." % cn)
md.append("Куда переносить: если якорного кода в новом методе больше нет — он, вероятно, вынесен/отрефакторен (ищите в диффе новый вызов/процедуру). Размести адаптацию по смыслу: например пост-обработкой после нового вызова, либо в заимствованной процедуре, куда переехал код. При правке файла сохрани кодировку (UTF-8 с BOM).") md.append("Куда переносить: если якорного кода в новом методе больше нет — он, вероятно, вынесен/отрефакторен (ищите в диффе новый вызов/процедуру). Размести адаптацию по смыслу: например пост-обработкой после нового вызова, либо в заимствованной процедуре, куда переехал код. При правке файла сохрани кодировку (UTF-8 с BOM).")
else: else:
@@ -1136,17 +1248,23 @@ def resync_one(ext_bsl, ext_lines, dup, method, logical_module, conflict_folder,
method_id = "%s.%s" % (logical_module, method["canonical"]) method_id = "%s.%s" % (logical_module, method["canonical"])
dec_line = dup["line"] dec_line = dup["line"]
sig_line_idx = dec_line + 1 sig_line_idx = dec_line + 1
name_re = re.compile(r'^\s*(?:Асинх\s+)?(?:Процедура|Функция)\s+([\w]+)\s*\(', re.IGNORECASE) name_re = re.compile(r'^\s*(?:' + kw_alt("Async") + r'\s+)?(' + kw_alt("Proc") + '|' + kw_alt("Func")
+ r')\s+([\w]+)\s*\(', re.IGNORECASE)
m0 = name_re.match(ext_lines[sig_line_idx]) if sig_line_idx < len(ext_lines) else None m0 = name_re.match(ext_lines[sig_line_idx]) if sig_line_idx < len(ext_lines) else None
if not m0: if not m0:
return {"id": method_id, "status": "ОШИБКА", "ext_bsl": ext_bsl, "reason": "не разобрать сигнатуру перехватчика"} return {"id": method_id, "status": "ОШИБКА", "ext_bsl": ext_bsl, "reason": "не разобрать сигнатуру перехватчика"}
existing_name = m0.group(1) # The block we are about to rewrite is the interceptor already in the extension, so it is
# its spelling — not the source module's — that the rewritten block must keep.
lang = bsl_lang(m0.group(1))
kw = BSL_KW[lang]
existing_name = m0.group(2)
sig = read_signature(ext_lines, sig_line_idx) sig = read_signature(ext_lines, sig_line_idx)
if not sig: if not sig:
return {"id": method_id, "status": "ОШИБКА", "ext_bsl": ext_bsl, "reason": "не разобрать сигнатуру"} return {"id": method_id, "status": "ОШИБКА", "ext_bsl": ext_bsl, "reason": "не разобрать сигнатуру"}
ext_params_text, sig_end = sig ext_params_text, sig_end = sig
is_func = bool(re.match(r'^\s*(?:Асинх\s+)?Функция\b', ext_lines[sig_line_idx], re.IGNORECASE)) is_func = bool(re.match(r'^\s*(?:' + kw_alt("Async") + r'\s+)?' + kw_alt("Func") + r'\b',
end_re = re.compile(r'^\s*КонецФункции\b' if is_func else r'^\s*КонецПроцедуры\b', re.IGNORECASE) ext_lines[sig_line_idx], re.IGNORECASE))
end_re = re.compile(r'^\s*' + (kw_alt("EndFunc") if is_func else kw_alt("EndProc")) + r'\b', re.IGNORECASE)
block_end = -1 block_end = -1
for j in range(sig_end + 1, len(ext_lines)): for j in range(sig_end + 1, len(ext_lines)):
if end_re.match(ext_lines[j]): if end_re.match(ext_lines[j]):
@@ -1172,7 +1290,7 @@ def resync_one(ext_bsl, ext_lines, dup, method, logical_module, conflict_folder,
if not params_drift and control_key(v1) == control_key(v2): if not params_drift and control_key(v1) == control_key(v2):
return {"id": method_id, "status": "АКТУАЛЕН", "ext_bsl": ext_bsl} return {"id": method_id, "status": "АКТУАЛЕН", "ext_bsl": ext_bsl}
insert_top = []; insert_after = {}; del_start = set(); del_end = set(); disputed = []; transferred = 0; absorbed = 0; absorbed_notes = [] insert_top = []; insert_after = {}; del_start = {}; del_end = {}; disputed = []; transferred = 0; absorbed = 0; absorbed_notes = []
v2sig, v2map = significant_projection(v2norm) v2sig, v2map = significant_projection(v2norm)
for op in ops: for op in ops:
if op["kind"] == "insert": if op["kind"] == "insert":
@@ -1198,16 +1316,17 @@ def resync_one(ext_bsl, ext_lines, dup, method, logical_module, conflict_folder,
elif k is None: elif k is None:
dbefore = v1[max(0, after - 2):after + 1] if after >= 0 else [] dbefore = v1[max(0, after - 2):after + 1] if after >= 0 else []
dafter = v1[after + 1:after + 4] dafter = v1[after + 1:after + 4]
disputed.append({"kind": "insert", "lines": op["lines"], "before": dbefore, "after": dafter}) disputed.append({"kind": "insert", "lines": op["lines"], "open": op.get("open"),
"close": op.get("close"), "before": dbefore, "after": dafter})
elif k < 0: elif k < 0:
insert_top.append(op["lines"]); transferred += 1 insert_top.append(op); transferred += 1
else: else:
insert_after.setdefault(k, []).append(op["lines"]); transferred += 1 insert_after.setdefault(k, []).append(op); transferred += 1
else: else:
keys = v1norm[op["start"]:op["end"] + 1] keys = v1norm[op["start"]:op["end"] + 1]
p = find_unique_run(v2norm, keys) p = find_unique_run(v2norm, keys)
if p >= 0: if p >= 0:
del_start.add(p); del_end.add(p + len(keys) - 1); transferred += 1 del_start[p] = op.get("open"); del_end[p + len(keys) - 1] = op.get("close"); transferred += 1
else: else:
# Nearest significant neighbours; adjacency in the significant projection means the # Nearest significant neighbours; adjacency in the significant projection means the
# block is already cut (blanks/comments left behind don't matter). # block is already cut (blanks/comments left behind don't matter).
@@ -1239,18 +1358,25 @@ def resync_one(ext_bsl, ext_lines, dup, method, logical_module, conflict_folder,
return {"id": method_id, "status": st, "ext_bsl": ext_bsl, "transferred": transferred, return {"id": method_id, "status": st, "ext_bsl": ext_bsl, "transferred": transferred,
"absorbed": absorbed, "disputed": len(disputed), "reason": rsn, "absorbed_notes": absorbed_notes} "absorbed": absorbed, "disputed": len(disputed), "reason": rsn, "absorbed_notes": absorbed_notes}
# Marker lines come back as they were read (open/close); the keyword form is only a fallback
# for a block whose closing marker the module never had.
def mk(orig, key):
return ("#" + kw[key]) if not orig else orig
new_body = [] new_body = []
for blk in insert_top: for blk in insert_top:
new_body.append("#Вставка"); new_body.extend(blk); new_body.append("#КонецВставки") new_body.append(mk(blk.get("open"), "Insert")); new_body.extend(blk["lines"])
new_body.append(mk(blk.get("close"), "EndInsert"))
for k in range(len(v2)): for k in range(len(v2)):
if k in del_start: if k in del_start:
new_body.append("#Удаление") new_body.append(mk(del_start[k], "Delete"))
new_body.append(v2[k]) new_body.append(v2[k])
if k in del_end: if k in del_end:
new_body.append("#КонецУдаления") new_body.append(mk(del_end[k], "EndDelete"))
if k in insert_after: if k in insert_after:
for blk in insert_after[k]: for blk in insert_after[k]:
new_body.append("#Вставка"); new_body.extend(blk); new_body.append("#КонецВставки") new_body.append(mk(blk.get("open"), "Insert")); new_body.extend(blk["lines"])
new_body.append(mk(blk.get("close"), "EndInsert"))
if disputed: if disputed:
new_body.append("\t// [РЕСИНК-КОНФЛИКТ] блоки ниже не легли автоматически — перенесите вручную (по № см. conflict.md / index.md в merge-воркспейсе, путь в выводе).") new_body.append("\t// [РЕСИНК-КОНФЛИКТ] блоки ниже не легли автоматически — перенесите вручную (по № см. conflict.md / index.md в merge-воркспейсе, путь в выводе).")
cn = 0 cn = 0
@@ -1258,19 +1384,20 @@ def resync_one(ext_bsl, ext_lines, dup, method, logical_module, conflict_folder,
cn += 1 cn += 1
if d["kind"] == "insert": if d["kind"] == "insert":
new_body.append("\t// [РЕСИНК-КОНФЛИКТ №%d] вставка — исходный якорь изменён в новом оригинале." % cn) new_body.append("\t// [РЕСИНК-КОНФЛИКТ №%d] вставка — исходный якорь изменён в новом оригинале." % cn)
new_body.append("#Вставка"); new_body.extend(d["lines"]); new_body.append("#КонецВставки") new_body.append(mk(d.get("open"), "Insert")); new_body.extend(d["lines"])
new_body.append(mk(d.get("close"), "EndInsert"))
else: else:
new_body.append("\t// [РЕСИНК-КОНФЛИКТ №%d] удаление — строки не найдены в новом оригинале:" % cn) new_body.append("\t// [РЕСИНК-КОНФЛИКТ №%d] удаление — строки не найдены в новом оригинале:" % cn)
for l in d["lines"]: for l in d["lines"]:
new_body.append("\t// " + l.strip()) new_body.append("\t// " + l.strip())
async_prefix = "Асинх " if method["is_async"] else "" async_prefix = (kw["Async"] + " ") if method["is_async"] else ""
keyword = "Функция" if method["is_function"] else "Процедура" keyword = kw["Func"] if method["is_function"] else kw["Proc"]
end_keyword = "КонецФункции" if method["is_function"] else "КонецПроцедуры" end_keyword = kw["EndFunc"] if method["is_function"] else kw["EndProc"]
new_block = [] new_block = []
if method["context"]: if method["context"]:
new_block.append(method["context"]) new_block.append(method["context"])
new_block.append('&ИзменениеИКонтроль("%s")' % method["canonical"]) new_block.append('&%s("%s")' % (kw["Control"], method["canonical"]))
new_block.append("%s%s %s(%s)" % (async_prefix, keyword, existing_name, method["params_text"])) new_block.append("%s%s %s(%s)" % (async_prefix, keyword, existing_name, method["params_text"]))
new_block.extend(new_body) new_block.extend(new_body)
new_block.append(end_keyword) new_block.append(end_keyword)
@@ -1284,7 +1411,7 @@ def resync_one(ext_bsl, ext_lines, dup, method, logical_module, conflict_folder,
conflict_dir = None conflict_dir = None
if disputed: if disputed:
conflict_dir = conflict_folder conflict_dir = conflict_folder
write_conflict_folder(conflict_folder, method_id, ext_bsl, existing_name, method, v1, marked_body, v2, v1norm, v2norm, disputed) write_conflict_folder(conflict_folder, method_id, ext_bsl, existing_name, method, v1, marked_body, v2, v1norm, v2norm, disputed, lang)
if disputed: if disputed:
status = "ЧАСТИЧНО" status = "ЧАСТИЧНО"
elif transferred == 0 and absorbed > 0: elif transferred == 0 and absorbed > 0:
@@ -36,7 +36,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src\cfe\extname" python ".roo/skills/cfe-validate/scripts/cfe-validate.py" -ExtensionPath "src\cfe\extname"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src\cfe\extname\Configuration.xml" python ".roo/skills/cfe-validate/scripts/cfe-validate.py" -ExtensionPath "src\cfe\extname\Configuration.xml"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src\cfe\extname" -ConfigPath "src\cf" python ".roo/skills/cfe-validate/scripts/cfe-validate.py" -ExtensionPath "src\cfe\extname" -ConfigPath "src\cf"
``` ```
@@ -1,4 +1,4 @@
# cfe-validate v1.16 — Validate 1C configuration extension structure (CFE) # cfe-validate v1.17 — Validate 1C configuration extension structure (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
[CmdletBinding(PositionalBinding=$false)] [CmdletBinding(PositionalBinding=$false)]
param( param(
@@ -1299,12 +1299,50 @@ if ($versionRank -ge 219 -and $childObjNode) {
} }
} }
# Built-in language keywords in both spellings. The platform accepts either one in any module
# (pairs taken from the platform string tables), so a module written in English is ordinary
# source, not a broken one: we must read both and emit the spelling we read.
function Get-BslKeywords {
return @{
ru = @{
Async="Асинх"; Proc="Процедура"; EndProc="КонецПроцедуры"
Func="Функция"; EndFunc="КонецФункции"; Val="Знач"
Region="Область"; EndRegion="КонецОбласти"
If="Если"; Then="Тогда"; ElsIf="ИначеЕсли"; Else="Иначе"; EndIf="КонецЕсли"
# Platform spells the negation "Не"; we emit "НЕ" as this skill always has —
# the language is case-insensitive, and the lower case would churn every snapshot.
And="И"; Or="Или"; Not="НЕ"
Insert="Вставка"; EndInsert="КонецВставки"; Delete="Удаление"; EndDelete="КонецУдаления"
Before="Перед"; After="После"; Around="Вместо"; Control="ИзменениеИКонтроль"
Proceed="ПродолжитьВызов"; Return="Возврат"
# Not a keyword: name of the local the generated Instead-stub declares. Lives here so
# that the language of emitted text is decided in exactly one place.
ResultVar="Результат"
Directives=@("НаКлиенте", "НаСервере", "НаСервереБезКонтекста", "НаКлиентеНаСервереБезКонтекста", "НаКлиентеНаСервере")
}
en = @{
Async="Async"; Proc="Procedure"; EndProc="EndProcedure"
Func="Function"; EndFunc="EndFunction"; Val="Val"
Region="Region"; EndRegion="EndRegion"
If="If"; Then="Then"; ElsIf="ElsIf"; Else="Else"; EndIf="EndIf"
And="And"; Or="Or"; Not="Not"
Insert="Insert"; EndInsert="EndInsert"; Delete="Delete"; EndDelete="EndDelete"
Before="Before"; After="After"; Around="Around"; Control="ChangeAndValidate"
Proceed="ProceedWithCall"; Return="Return"
ResultVar="Result"
Directives=@("AtClient", "AtServer", "AtServerNoContext", "AtClientAtServerNoContext", "AtClientAtServer")
}
}
}
# --- Breadcrumb: controlled methods (&ИзменениеИКонтроль) drift is not checked here --- # --- Breadcrumb: controlled methods (&ИзменениеИКонтроль) drift is not checked here ---
$extRootDir = Split-Path $resolvedPath -Parent $extRootDir = Split-Path $resolvedPath -Parent
$ctrlKw = Get-BslKeywords
$ctrlRe = '(?im)^\s*&(?:' + $ctrlKw.ru.Control + '|' + $ctrlKw.en.Control + ')\('
$ctrlCount = 0 $ctrlCount = 0
foreach ($bslFile in (Get-ChildItem -Path $extRootDir -Recurse -Filter *.bsl -File -ErrorAction SilentlyContinue)) { foreach ($bslFile in (Get-ChildItem -Path $extRootDir -Recurse -Filter *.bsl -File -ErrorAction SilentlyContinue)) {
$txt = [System.IO.File]::ReadAllText($bslFile.FullName, [System.Text.Encoding]::UTF8) $txt = [System.IO.File]::ReadAllText($bslFile.FullName, [System.Text.Encoding]::UTF8)
$ctrlCount += ([regex]::Matches($txt, '(?m)^\s*&ИзменениеИКонтроль\(')).Count $ctrlCount += ([regex]::Matches($txt, $ctrlRe)).Count
} }
if ($ctrlCount -gt 0) { if ($ctrlCount -gt 0) {
Out-Line "[INFO] Контролируемых методов (&ИзменениеИКонтроль): $ctrlCount — их актуальность здесь не проверяется. Сверьте: /cfe-patch-method -Check -ExtensionPath <ext> -ConfigPath <cf>" Out-Line "[INFO] Контролируемых методов (&ИзменениеИКонтроль): $ctrlCount — их актуальность здесь не проверяется. Сверьте: /cfe-patch-method -Check -ExtensionPath <ext> -ConfigPath <cf>"
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# cfe-validate v1.16 — Validate 1C configuration extension XML structure (CFE) # cfe-validate v1.17 — Validate 1C configuration extension XML structure (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Validates extension Configuration.xml: root, InternalInfo, extension properties, ChildObjects, borrowed objects.""" """Validates extension Configuration.xml: root, InternalInfo, extension properties, ChildObjects, borrowed objects."""
import sys, os, argparse, re import sys, os, argparse, re
@@ -27,6 +27,42 @@ def ci_parse_args(parser, argv=None):
return parser.parse_args(argv) return parser.parse_args(argv)
# Built-in language keywords in both spellings. The platform accepts either one in any module
# (pairs taken from the platform string tables), so a module written in English is ordinary
# source, not a broken one: we must read both and emit the spelling we read.
def bsl_keywords():
return {
"ru": {
"Async": "Асинх", "Proc": "Процедура", "EndProc": "КонецПроцедуры",
"Func": "Функция", "EndFunc": "КонецФункции", "Val": "Знач",
"Region": "Область", "EndRegion": "КонецОбласти",
"If": "Если", "Then": "Тогда", "ElsIf": "ИначеЕсли", "Else": "Иначе", "EndIf": "КонецЕсли",
# Platform spells the negation "Не"; we emit "НЕ" as this skill always has —
# the language is case-insensitive, and the lower case would churn every snapshot.
"And": "И", "Or": "Или", "Not": "НЕ",
"Insert": "Вставка", "EndInsert": "КонецВставки", "Delete": "Удаление", "EndDelete": "КонецУдаления",
"Before": "Перед", "After": "После", "Around": "Вместо", "Control": "ИзменениеИКонтроль",
"Proceed": "ПродолжитьВызов", "Return": "Возврат",
# Not a keyword: name of the local the generated Instead-stub declares. Lives here so
# that the language of emitted text is decided in exactly one place.
"ResultVar": "Результат",
"Directives": ["НаКлиенте", "НаСервере", "НаСервереБезКонтекста", "НаКлиентеНаСервереБезКонтекста", "НаКлиентеНаСервере"],
},
"en": {
"Async": "Async", "Proc": "Procedure", "EndProc": "EndProcedure",
"Func": "Function", "EndFunc": "EndFunction", "Val": "Val",
"Region": "Region", "EndRegion": "EndRegion",
"If": "If", "Then": "Then", "ElsIf": "ElsIf", "Else": "Else", "EndIf": "EndIf",
"And": "And", "Or": "Or", "Not": "Not",
"Insert": "Insert", "EndInsert": "EndInsert", "Delete": "Delete", "EndDelete": "EndDelete",
"Before": "Before", "After": "After", "Around": "Around", "Control": "ChangeAndValidate",
"Proceed": "ProceedWithCall", "Return": "Return",
"ResultVar": "Result",
"Directives": ["AtClient", "AtServer", "AtServerNoContext", "AtClientAtServerNoContext", "AtClientAtServer"],
},
}
NS = { NS = {
'md': 'http://v8.1c.ru/8.3/MDClasses', 'md': 'http://v8.1c.ru/8.3/MDClasses',
'v8': 'http://v8.1c.ru/8.1/data/core', 'v8': 'http://v8.1c.ru/8.1/data/core',
@@ -1267,6 +1303,8 @@ def main():
r.warn(f'16. {issue}') r.warn(f'16. {issue}')
# --- Breadcrumb: controlled methods (&ИзменениеИКонтроль) drift is not checked here --- # --- Breadcrumb: controlled methods (&ИзменениеИКонтроль) drift is not checked here ---
ctrl_kw = bsl_keywords()
ctrl_re = re.compile(r'^\s*&(?:' + ctrl_kw["ru"]["Control"] + '|' + ctrl_kw["en"]["Control"] + r')\(', re.IGNORECASE)
ctrl_count = 0 ctrl_count = 0
for dp, _dn, files in os.walk(config_dir): for dp, _dn, files in os.walk(config_dir):
for fn in files: for fn in files:
@@ -1274,7 +1312,7 @@ def main():
try: try:
with open(os.path.join(dp, fn), 'r', encoding='utf-8-sig') as f: with open(os.path.join(dp, fn), 'r', encoding='utf-8-sig') as f:
for ln in f: for ln in f:
if re.match(r'^\s*&ИзменениеИКонтроль\(', ln): if ctrl_re.match(ln):
ctrl_count += 1 ctrl_count += 1
except OSError: except OSError:
pass pass
@@ -25,18 +25,18 @@ allowed-tools:
## Параметры подключения ## Параметры подключения
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: Прочитай `.v8-project.json` из корня проекта и разреши базу:
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую 1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` 2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default` 4. Если ветка не совпала — используй `default`
Если `v8path` не задан — скрипт сам попытается определить платформу. Платформа — `v8path` найденной записи базы, иначе корневой `v8path`. Не задан ни там, ни там — `-V8Path` не передавай.
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-cfe-admin.ps1" -Command <команда> <параметры> python ".roo/skills/db-cfe-admin/scripts/db-cfe-admin.py" -Command <команда> <параметры>
``` ```
### Общие параметры ### Общие параметры
@@ -1,4 +1,4 @@
# db-cfe-admin v1.0 — Configuration extensions in a 1C infobase: list, check, properties, delete # db-cfe-admin v1.1 — Configuration extensions in a 1C infobase: list, check, properties, delete
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
@@ -110,9 +110,10 @@ $OutputEncoding = [System.Text.Encoding]::UTF8
# Копии держит одинаковыми tests/skills/check-inline-drift.mjs — правку вносить в навык-эталон. # Копии держит одинаковыми tests/skills/check-inline-drift.mjs — правку вносить в навык-эталон.
$Extension = $Name $Extension = $Name
# --- Реквизиты хранилища из .v8-project.json --- # --- Запись базы в .v8-project.json ---
# Модель их не передаёт: скрипт сопоставляет параметры соединения с записью в databases[] # Модель не передаёт ни путь к платформе конкретной базы, ни реквизиты хранилища: скрипт
# и берёт repository оттуда. Тот же приём, что в cf-edit.ps1 (сопоставление по configSrc). # сопоставляет параметры соединения с записью в databases[] и берёт их оттуда. Тот же приём,
# что в cf-edit.ps1 (сопоставление по configSrc).
function Find-V8Project([string]$startDir) { function Find-V8Project([string]$startDir) {
$d = $startDir $d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) { for ($i = 0; $i -lt 20 -and $d; $i++) {
@@ -396,6 +397,10 @@ Assert-InfoBaseExists $InfoBasePath
# --- Resolve V8Path --- # --- Resolve V8Path ---
function Find-ProjectV8Path { function Find-ProjectV8Path {
# v8path записи базы сильнее корневого: в одном проекте базы живут на разных версиях
# платформы, а версию формата выгрузки задаёт та платформа, которая выгружает.
$dbRec = Find-ProjectDatabase
if ($dbRec -and $dbRec.v8path) { return [string]$dbRec.v8path }
$dir = (Get-Location).Path $dir = (Get-Location).Path
while ($dir) { while ($dir) {
$pf = Join-Path $dir ".v8-project.json" $pf = Join-Path $dir ".v8-project.json"
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-cfe-admin v1.0 — Configuration extensions in a 1C infobase: list, check, properties, delete # db-cfe-admin v1.1 — Configuration extensions in a 1C infobase: list, check, properties, delete
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -38,8 +38,15 @@ def ci_parse_args(parser, argv=None):
def _find_project_v8path(): def _find_project_v8path(args):
"""Walk up from CWD to find .v8-project.json and read its v8path.""" """Walk up from CWD to find .v8-project.json and read its v8path.
v8path записи базы сильнее корневого: в одном проекте базы живут на разных версиях
платформы, а версию формата выгрузки задаёт та платформа, которая выгружает.
"""
db = find_project_database(args)
if db and db.get("v8path"):
return db["v8path"]
d = os.getcwd() d = os.getcwd()
while True: while True:
pf = os.path.join(d, ".v8-project.json") pf = os.path.join(d, ".v8-project.json")
@@ -88,9 +95,10 @@ V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP", "/ConfigurationRepositoryP"]
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"] IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
# --- Реквизиты хранилища из .v8-project.json --- # --- Запись базы в .v8-project.json ---
# Модель их не передаёт: скрипт сопоставляет параметры соединения с записью в databases[] # Модель не передаёт ни путь к платформе конкретной базы, ни реквизиты хранилища: скрипт
# и берёт repository оттуда. Тот же приём, что в cf-edit.py (сопоставление по configSrc). # сопоставляет параметры соединения с записью в databases[] и берёт их оттуда. Тот же приём,
# что в cf-edit.py (сопоставление по configSrc).
def _sg_find_v8project(start_dir): def _sg_find_v8project(start_dir):
d = start_dir d = start_dir
for _ in range(20): for _ in range(20):
@@ -329,10 +337,10 @@ def _version_key(p):
return [int(x) for x in re.findall(r"\d+", _version_dir(p))] return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
def resolve_v8path(v8path): def resolve_v8path(v8path, args):
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly).""" """Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path: if not v8path:
v8path = _find_project_v8path() v8path = _find_project_v8path(args)
if not v8path: if not v8path:
if os.name == "nt": if os.name == "nt":
candidates = ( candidates = (
@@ -629,7 +637,7 @@ def main():
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath") args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
assert_infobase_exists(args.InfoBasePath) assert_infobase_exists(args.InfoBasePath)
v8path = resolve_v8path(args.V8Path) v8path = resolve_v8path(args.V8Path, args)
# --- Утилиты платформы: нужны обе, выбор по команде --- # --- Утилиты платформы: нужны обе, выбор по команде ---
# -V8Path указывает на каталог bin либо на любой из двух файлов; второй берётся соседом. # -V8Path указывает на каталог bin либо на любой из двух файлов; второй берётся соседом.
@@ -24,14 +24,14 @@ allowed-tools:
## Параметры подключения ## Параметры подключения
Прочитай `.v8-project.json` из корня проекта для `v8path` (путь к платформе). Прочитай `.v8-project.json` из корня проекта. Платформа — `v8path` записи создаваемой базы,
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files). если она уже в реестре, иначе корневой `v8path`. Не задан ни там, ни там — `-V8Path` не передавай.
После создания базы предложи зарегистрировать через `/db-list add`. После создания базы предложи зарегистрировать через `/db-list add`.
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" <параметры> python ".roo/skills/db-create/scripts/db-create.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -59,14 +59,14 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" <п
```powershell ```powershell
# Создать файловую базу # Создать файловую базу
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" python ".roo/skills/db-create/scripts/db-create.py" -InfoBasePath "C:\Bases\NewDB"
# Создать серверную базу # Создать серверную базу
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" python ".roo/skills/db-create/scripts/db-create.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test"
# Создать из шаблона CF # Создать из шаблона CF
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf" python ".roo/skills/db-create/scripts/db-create.py" -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf"
# Создать и добавить в список баз # Создать и добавить в список баз
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" -AddToList -ListName "Новая база" python ".roo/skills/db-create/scripts/db-create.py" -InfoBasePath "C:\Bases\NewDB" -AddToList -ListName "Новая база"
``` ```
@@ -1,4 +1,4 @@
# db-create v1.14 — Create 1C information base # db-create v1.15 — Create 1C information base
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
@@ -243,8 +243,52 @@ $V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
$InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath' $InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath'
$UseTemplate = ConvertTo-CleanPath $UseTemplate '-UseTemplate' $UseTemplate = ConvertTo-CleanPath $UseTemplate '-UseTemplate'
# --- Запись базы в .v8-project.json ---
# Модель не передаёт ни путь к платформе конкретной базы, ни реквизиты хранилища: скрипт
# сопоставляет параметры соединения с записью в databases[] и берёт их оттуда. Тот же приём,
# что в cf-edit.ps1 (сопоставление по configSrc).
function Find-V8Project([string]$startDir) {
$d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) {
$pj = Join-Path $d ".v8-project.json"
if (Test-Path $pj) { return $pj }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
return $null
}
function Test-SamePath {
param([string]$A, [string]$B)
if (-not $A -or -not $B) { return $false }
try {
$na = [System.IO.Path]::GetFullPath($A).TrimEnd('\', '/')
$nb = [System.IO.Path]::GetFullPath($B).TrimEnd('\', '/')
return $na.Equals($nb, [System.StringComparison]::OrdinalIgnoreCase)
} catch { return $false }
}
function Find-ProjectDatabase {
# Запись базы в реестре, соответствующая переданному соединению. $null, если не найдена.
$pf = Find-V8Project (Get-Location).Path
if (-not $pf) { return $null }
try { $proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json } catch { return $null }
if (-not $proj.databases) { return $null }
foreach ($db in $proj.databases) {
if ($InfoBasePath -and $db.path -and (Test-SamePath $db.path $InfoBasePath)) { return $db }
if ($InfoBaseServer -and $InfoBaseRef -and $db.server -and $db.ref) {
if ($db.server.Equals($InfoBaseServer, [System.StringComparison]::OrdinalIgnoreCase) -and
$db.ref.Equals($InfoBaseRef, [System.StringComparison]::OrdinalIgnoreCase)) { return $db }
}
}
return $null
}
# --- Resolve V8Path --- # --- Resolve V8Path ---
function Find-ProjectV8Path { function Find-ProjectV8Path {
# v8path записи базы сильнее корневого: в одном проекте базы живут на разных версиях
# платформы, а версию формата выгрузки задаёт та платформа, которая выгружает.
$dbRec = Find-ProjectDatabase
if ($dbRec -and $dbRec.v8path) { return [string]$dbRec.v8path }
$dir = (Get-Location).Path $dir = (Get-Location).Path
while ($dir) { while ($dir) {
$pf = Join-Path $dir ".v8-project.json" $pf = Join-Path $dir ".v8-project.json"
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-create v1.14 — Create 1C information base # db-create v1.15 — Create 1C information base
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -37,8 +37,62 @@ def ci_parse_args(parser, argv=None):
def _find_project_v8path(): # --- Запись базы в .v8-project.json ---
"""Walk up from CWD to find .v8-project.json and read its v8path.""" # Модель не передаёт ни путь к платформе конкретной базы, ни реквизиты хранилища: скрипт
# сопоставляет параметры соединения с записью в databases[] и берёт их оттуда. Тот же приём,
# что в cf-edit.py (сопоставление по configSrc).
def _sg_find_v8project(start_dir):
d = start_dir
for _ in range(20):
if not d:
break
pj = os.path.join(d, ".v8-project.json")
if os.path.isfile(pj):
return pj
parent = os.path.dirname(d)
if parent == d:
break
d = parent
return None
def same_path(a, b):
if not a or not b:
return False
try:
return os.path.abspath(a).rstrip("\\/").lower() == os.path.abspath(b).rstrip("\\/").lower()
except Exception:
return False
def find_project_database(args):
"""Запись базы в реестре, соответствующая переданному соединению. None, если не найдена."""
pf = _sg_find_v8project(os.getcwd())
if not pf:
return None
try:
with open(pf, encoding="utf-8-sig") as f:
proj = json.load(f)
except Exception:
return None
for db in proj.get("databases") or []:
if args.InfoBasePath and db.get("path") and same_path(db["path"], args.InfoBasePath):
return db
if args.InfoBaseServer and args.InfoBaseRef and db.get("server") and db.get("ref"):
if (db["server"].lower() == args.InfoBaseServer.lower()
and db["ref"].lower() == args.InfoBaseRef.lower()):
return db
return None
def _find_project_v8path(args):
"""Walk up from CWD to find .v8-project.json and read its v8path.
v8path записи базы сильнее корневого: в одном проекте базы живут на разных версиях
платформы, а версию формата выгрузки задаёт та платформа, которая выгружает.
"""
db = find_project_database(args)
if db and db.get("v8path"):
return db["v8path"]
d = os.getcwd() d = os.getcwd()
while True: while True:
pf = os.path.join(d, ".v8-project.json") pf = os.path.join(d, ".v8-project.json")
@@ -243,10 +297,10 @@ def _version_key(p):
return [int(x) for x in re.findall(r"\d+", _version_dir(p))] return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
def resolve_v8path(v8path): def resolve_v8path(v8path, args):
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly).""" """Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path: if not v8path:
v8path = _find_project_v8path() v8path = _find_project_v8path(args)
if not v8path: if not v8path:
if os.name == "nt": if os.name == "nt":
candidates = ( candidates = (
@@ -418,7 +472,7 @@ def main():
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath") args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
args.UseTemplate = clean_path(args.UseTemplate, "-UseTemplate") args.UseTemplate = clean_path(args.UseTemplate, "-UseTemplate")
v8path = resolve_v8path(args.V8Path) v8path = resolve_v8path(args.V8Path, args)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8" engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Resolve additional arguments for the selected engine --- # --- Resolve additional arguments for the selected engine ---
@@ -23,19 +23,19 @@ allowed-tools:
## Параметры подключения ## Параметры подключения
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: Прочитай `.v8-project.json` из корня проекта и разреши базу:
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую 1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` 2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default` 4. Если ветка не совпала — используй `default`
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files). Платформа — `v8path` найденной записи базы, иначе корневой `v8path`. Не задан ни там, ни там — `-V8Path` не передавай.
Если файла нет — предложи `/db-list add`. Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" <параметры> python ".roo/skills/db-dump-cf/scripts/db-dump-cf.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -60,11 +60,11 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" <п
```powershell ```powershell
# Выгрузка конфигурации (файловая база) # Выгрузка конфигурации (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\config.cf" python ".roo/skills/db-dump-cf/scripts/db-dump-cf.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\config.cf"
# Серверная база # Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "config.cf" python ".roo/skills/db-dump-cf/scripts/db-dump-cf.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "config.cf"
# Выгрузка расширения # Выгрузка расширения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "ext.cfe" -Extension "МоёРасширение" python ".roo/skills/db-dump-cf/scripts/db-dump-cf.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "ext.cfe" -Extension "МоёРасширение"
``` ```
@@ -1,4 +1,4 @@
# db-dump-cf v1.16 — Dump 1C configuration to CF file # db-dump-cf v1.17 — Dump 1C configuration to CF file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
@@ -272,8 +272,52 @@ function Assert-InfoBaseExists {
Assert-InfoBaseExists $InfoBasePath Assert-InfoBaseExists $InfoBasePath
# --- Запись базы в .v8-project.json ---
# Модель не передаёт ни путь к платформе конкретной базы, ни реквизиты хранилища: скрипт
# сопоставляет параметры соединения с записью в databases[] и берёт их оттуда. Тот же приём,
# что в cf-edit.ps1 (сопоставление по configSrc).
function Find-V8Project([string]$startDir) {
$d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) {
$pj = Join-Path $d ".v8-project.json"
if (Test-Path $pj) { return $pj }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
return $null
}
function Test-SamePath {
param([string]$A, [string]$B)
if (-not $A -or -not $B) { return $false }
try {
$na = [System.IO.Path]::GetFullPath($A).TrimEnd('\', '/')
$nb = [System.IO.Path]::GetFullPath($B).TrimEnd('\', '/')
return $na.Equals($nb, [System.StringComparison]::OrdinalIgnoreCase)
} catch { return $false }
}
function Find-ProjectDatabase {
# Запись базы в реестре, соответствующая переданному соединению. $null, если не найдена.
$pf = Find-V8Project (Get-Location).Path
if (-not $pf) { return $null }
try { $proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json } catch { return $null }
if (-not $proj.databases) { return $null }
foreach ($db in $proj.databases) {
if ($InfoBasePath -and $db.path -and (Test-SamePath $db.path $InfoBasePath)) { return $db }
if ($InfoBaseServer -and $InfoBaseRef -and $db.server -and $db.ref) {
if ($db.server.Equals($InfoBaseServer, [System.StringComparison]::OrdinalIgnoreCase) -and
$db.ref.Equals($InfoBaseRef, [System.StringComparison]::OrdinalIgnoreCase)) { return $db }
}
}
return $null
}
# --- Resolve V8Path --- # --- Resolve V8Path ---
function Find-ProjectV8Path { function Find-ProjectV8Path {
# v8path записи базы сильнее корневого: в одном проекте базы живут на разных версиях
# платформы, а версию формата выгрузки задаёт та платформа, которая выгружает.
$dbRec = Find-ProjectDatabase
if ($dbRec -and $dbRec.v8path) { return [string]$dbRec.v8path }
$dir = (Get-Location).Path $dir = (Get-Location).Path
while ($dir) { while ($dir) {
$pf = Join-Path $dir ".v8-project.json" $pf = Join-Path $dir ".v8-project.json"
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-dump-cf v1.16 — Dump 1C configuration to CF file # db-dump-cf v1.17 — Dump 1C configuration to CF file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -37,8 +37,62 @@ def ci_parse_args(parser, argv=None):
def _find_project_v8path(): # --- Запись базы в .v8-project.json ---
"""Walk up from CWD to find .v8-project.json and read its v8path.""" # Модель не передаёт ни путь к платформе конкретной базы, ни реквизиты хранилища: скрипт
# сопоставляет параметры соединения с записью в databases[] и берёт их оттуда. Тот же приём,
# что в cf-edit.py (сопоставление по configSrc).
def _sg_find_v8project(start_dir):
d = start_dir
for _ in range(20):
if not d:
break
pj = os.path.join(d, ".v8-project.json")
if os.path.isfile(pj):
return pj
parent = os.path.dirname(d)
if parent == d:
break
d = parent
return None
def same_path(a, b):
if not a or not b:
return False
try:
return os.path.abspath(a).rstrip("\\/").lower() == os.path.abspath(b).rstrip("\\/").lower()
except Exception:
return False
def find_project_database(args):
"""Запись базы в реестре, соответствующая переданному соединению. None, если не найдена."""
pf = _sg_find_v8project(os.getcwd())
if not pf:
return None
try:
with open(pf, encoding="utf-8-sig") as f:
proj = json.load(f)
except Exception:
return None
for db in proj.get("databases") or []:
if args.InfoBasePath and db.get("path") and same_path(db["path"], args.InfoBasePath):
return db
if args.InfoBaseServer and args.InfoBaseRef and db.get("server") and db.get("ref"):
if (db["server"].lower() == args.InfoBaseServer.lower()
and db["ref"].lower() == args.InfoBaseRef.lower()):
return db
return None
def _find_project_v8path(args):
"""Walk up from CWD to find .v8-project.json and read its v8path.
v8path записи базы сильнее корневого: в одном проекте базы живут на разных версиях
платформы, а версию формата выгрузки задаёт та платформа, которая выгружает.
"""
db = find_project_database(args)
if db and db.get("v8path"):
return db["v8path"]
d = os.getcwd() d = os.getcwd()
while True: while True:
pf = os.path.join(d, ".v8-project.json") pf = os.path.join(d, ".v8-project.json")
@@ -243,10 +297,10 @@ def _version_key(p):
return [int(x) for x in re.findall(r"\d+", _version_dir(p))] return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
def resolve_v8path(v8path): def resolve_v8path(v8path, args):
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly).""" """Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path: if not v8path:
v8path = _find_project_v8path() v8path = _find_project_v8path(args)
if not v8path: if not v8path:
if os.name == "nt": if os.name == "nt":
candidates = ( candidates = (
@@ -439,7 +493,7 @@ def main():
assert_infobase_exists(args.InfoBasePath) assert_infobase_exists(args.InfoBasePath)
args.OutputFile = clean_path(args.OutputFile, "-OutputFile") args.OutputFile = clean_path(args.OutputFile, "-OutputFile")
v8path = resolve_v8path(args.V8Path) v8path = resolve_v8path(args.V8Path, args)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8" engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Resolve additional arguments for the selected engine --- # --- Resolve additional arguments for the selected engine ---
@@ -26,19 +26,19 @@ allowed-tools:
## Параметры подключения ## Параметры подключения
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: Прочитай `.v8-project.json` из корня проекта и разреши базу:
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую 1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` 2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default` 4. Если ветка не совпала — используй `default`
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files). Платформа — `v8path` найденной записи базы, иначе корневой `v8path`. Не задан ни там, ни там — `-V8Path` не передавай.
Если файла нет — предложи `/db-list add`. Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" <параметры> python ".roo/skills/db-dump-dt/scripts/db-dump-dt.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -61,10 +61,10 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" <п
```powershell ```powershell
# Выгрузка ИБ (файловая база) # Выгрузка ИБ (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\base.dt" python ".roo/skills/db-dump-dt/scripts/db-dump-dt.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\base.dt"
# Серверная база # Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "base.dt" python ".roo/skills/db-dump-dt/scripts/db-dump-dt.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "base.dt"
``` ```
## Связанные навыки ## Связанные навыки
@@ -1,4 +1,4 @@
# db-dump-dt v1.15 — Dump 1C information base to DT file # db-dump-dt v1.16 — Dump 1C information base to DT file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
@@ -256,8 +256,52 @@ function Assert-InfoBaseExists {
Assert-InfoBaseExists $InfoBasePath Assert-InfoBaseExists $InfoBasePath
# --- Запись базы в .v8-project.json ---
# Модель не передаёт ни путь к платформе конкретной базы, ни реквизиты хранилища: скрипт
# сопоставляет параметры соединения с записью в databases[] и берёт их оттуда. Тот же приём,
# что в cf-edit.ps1 (сопоставление по configSrc).
function Find-V8Project([string]$startDir) {
$d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) {
$pj = Join-Path $d ".v8-project.json"
if (Test-Path $pj) { return $pj }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
return $null
}
function Test-SamePath {
param([string]$A, [string]$B)
if (-not $A -or -not $B) { return $false }
try {
$na = [System.IO.Path]::GetFullPath($A).TrimEnd('\', '/')
$nb = [System.IO.Path]::GetFullPath($B).TrimEnd('\', '/')
return $na.Equals($nb, [System.StringComparison]::OrdinalIgnoreCase)
} catch { return $false }
}
function Find-ProjectDatabase {
# Запись базы в реестре, соответствующая переданному соединению. $null, если не найдена.
$pf = Find-V8Project (Get-Location).Path
if (-not $pf) { return $null }
try { $proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json } catch { return $null }
if (-not $proj.databases) { return $null }
foreach ($db in $proj.databases) {
if ($InfoBasePath -and $db.path -and (Test-SamePath $db.path $InfoBasePath)) { return $db }
if ($InfoBaseServer -and $InfoBaseRef -and $db.server -and $db.ref) {
if ($db.server.Equals($InfoBaseServer, [System.StringComparison]::OrdinalIgnoreCase) -and
$db.ref.Equals($InfoBaseRef, [System.StringComparison]::OrdinalIgnoreCase)) { return $db }
}
}
return $null
}
# --- Resolve V8Path --- # --- Resolve V8Path ---
function Find-ProjectV8Path { function Find-ProjectV8Path {
# v8path записи базы сильнее корневого: в одном проекте базы живут на разных версиях
# платформы, а версию формата выгрузки задаёт та платформа, которая выгружает.
$dbRec = Find-ProjectDatabase
if ($dbRec -and $dbRec.v8path) { return [string]$dbRec.v8path }
$dir = (Get-Location).Path $dir = (Get-Location).Path
while ($dir) { while ($dir) {
$pf = Join-Path $dir ".v8-project.json" $pf = Join-Path $dir ".v8-project.json"
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-dump-dt v1.15 — Dump 1C information base to DT file # db-dump-dt v1.16 — Dump 1C information base to DT file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -37,8 +37,62 @@ def ci_parse_args(parser, argv=None):
def _find_project_v8path(): # --- Запись базы в .v8-project.json ---
"""Walk up from CWD to find .v8-project.json and read its v8path.""" # Модель не передаёт ни путь к платформе конкретной базы, ни реквизиты хранилища: скрипт
# сопоставляет параметры соединения с записью в databases[] и берёт их оттуда. Тот же приём,
# что в cf-edit.py (сопоставление по configSrc).
def _sg_find_v8project(start_dir):
d = start_dir
for _ in range(20):
if not d:
break
pj = os.path.join(d, ".v8-project.json")
if os.path.isfile(pj):
return pj
parent = os.path.dirname(d)
if parent == d:
break
d = parent
return None
def same_path(a, b):
if not a or not b:
return False
try:
return os.path.abspath(a).rstrip("\\/").lower() == os.path.abspath(b).rstrip("\\/").lower()
except Exception:
return False
def find_project_database(args):
"""Запись базы в реестре, соответствующая переданному соединению. None, если не найдена."""
pf = _sg_find_v8project(os.getcwd())
if not pf:
return None
try:
with open(pf, encoding="utf-8-sig") as f:
proj = json.load(f)
except Exception:
return None
for db in proj.get("databases") or []:
if args.InfoBasePath and db.get("path") and same_path(db["path"], args.InfoBasePath):
return db
if args.InfoBaseServer and args.InfoBaseRef and db.get("server") and db.get("ref"):
if (db["server"].lower() == args.InfoBaseServer.lower()
and db["ref"].lower() == args.InfoBaseRef.lower()):
return db
return None
def _find_project_v8path(args):
"""Walk up from CWD to find .v8-project.json and read its v8path.
v8path записи базы сильнее корневого: в одном проекте базы живут на разных версиях
платформы, а версию формата выгрузки задаёт та платформа, которая выгружает.
"""
db = find_project_database(args)
if db and db.get("v8path"):
return db["v8path"]
d = os.getcwd() d = os.getcwd()
while True: while True:
pf = os.path.join(d, ".v8-project.json") pf = os.path.join(d, ".v8-project.json")
@@ -243,10 +297,10 @@ def _version_key(p):
return [int(x) for x in re.findall(r"\d+", _version_dir(p))] return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
def resolve_v8path(v8path): def resolve_v8path(v8path, args):
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly).""" """Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path: if not v8path:
v8path = _find_project_v8path() v8path = _find_project_v8path(args)
if not v8path: if not v8path:
if os.name == "nt": if os.name == "nt":
candidates = ( candidates = (
@@ -437,7 +491,7 @@ def main():
assert_infobase_exists(args.InfoBasePath) assert_infobase_exists(args.InfoBasePath)
args.OutputFile = clean_path(args.OutputFile, "-OutputFile") args.OutputFile = clean_path(args.OutputFile, "-OutputFile")
v8path = resolve_v8path(args.V8Path) v8path = resolve_v8path(args.V8Path, args)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8" engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Resolve additional arguments for the selected engine --- # --- Resolve additional arguments for the selected engine ---
@@ -24,12 +24,12 @@ allowed-tools:
## Параметры подключения ## Параметры подключения
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: Прочитай `.v8-project.json` из корня проекта и разреши базу:
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую 1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` 2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default` 4. Если ветка не совпала — используй `default`
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files). Платформа — `v8path` найденной записи базы, иначе корневой `v8path`. Не задан ни там, ни там — `-V8Path` не передавай.
Если файла нет — предложи `/db-list add`. Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
Если в записи базы указан `configSrc` — используй как каталог выгрузки по умолчанию. Если в записи базы указан `configSrc` — используй как каталог выгрузки по умолчанию.
@@ -38,7 +38,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" <параметры> python ".roo/skills/db-dump-xml/scripts/db-dump-xml.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -77,17 +77,17 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" <
```powershell ```powershell
# Полная выгрузка (файловая база) # Полная выгрузка (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full python ".roo/skills/db-dump-xml/scripts/db-dump-xml.py" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full
# Инкрементальная выгрузка # Инкрементальная выгрузка
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Changes python ".roo/skills/db-dump-xml/scripts/db-dump-xml.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Changes
# Частичная выгрузка # Частичная выгрузка
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ" python ".roo/skills/db-dump-xml/scripts/db-dump-xml.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ"
# Серверная база # Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -ConfigDir "C:\WS\cfsrc" -Mode Full python ".roo/skills/db-dump-xml/scripts/db-dump-xml.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -ConfigDir "C:\WS\cfsrc" -Mode Full
# Выгрузка расширения # Выгрузка расширения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение" python ".roo/skills/db-dump-xml/scripts/db-dump-xml.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
``` ```
@@ -1,4 +1,4 @@
# db-dump-xml v1.21 — Dump 1C configuration to XML files # db-dump-xml v1.22 — Dump 1C configuration to XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
@@ -125,9 +125,10 @@ param(
$OutputEncoding = [System.Text.Encoding]::UTF8 $OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Реквизиты хранилища из .v8-project.json --- # --- Запись базы в .v8-project.json ---
# Модель их не передаёт: скрипт сопоставляет параметры соединения с записью в databases[] # Модель не передаёт ни путь к платформе конкретной базы, ни реквизиты хранилища: скрипт
# и берёт repository оттуда. Тот же приём, что в cf-edit.ps1 (сопоставление по configSrc). # сопоставляет параметры соединения с записью в databases[] и берёт их оттуда. Тот же приём,
# что в cf-edit.ps1 (сопоставление по configSrc).
function Find-V8Project([string]$startDir) { function Find-V8Project([string]$startDir) {
$d = $startDir $d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) { for ($i = 0; $i -lt 20 -and $d; $i++) {
@@ -395,6 +396,10 @@ Assert-InfoBaseExists $InfoBasePath
# --- Resolve V8Path --- # --- Resolve V8Path ---
function Find-ProjectV8Path { function Find-ProjectV8Path {
# v8path записи базы сильнее корневого: в одном проекте базы живут на разных версиях
# платформы, а версию формата выгрузки задаёт та платформа, которая выгружает.
$dbRec = Find-ProjectDatabase
if ($dbRec -and $dbRec.v8path) { return [string]$dbRec.v8path }
$dir = (Get-Location).Path $dir = (Get-Location).Path
while ($dir) { while ($dir) {
$pf = Join-Path $dir ".v8-project.json" $pf = Join-Path $dir ".v8-project.json"
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-dump-xml v1.21 — Dump 1C configuration to XML files # db-dump-xml v1.22 — Dump 1C configuration to XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -37,8 +37,15 @@ def ci_parse_args(parser, argv=None):
def _find_project_v8path(): def _find_project_v8path(args):
"""Walk up from CWD to find .v8-project.json and read its v8path.""" """Walk up from CWD to find .v8-project.json and read its v8path.
v8path записи базы сильнее корневого: в одном проекте базы живут на разных версиях
платформы, а версию формата выгрузки задаёт та платформа, которая выгружает.
"""
db = find_project_database(args)
if db and db.get("v8path"):
return db["v8path"]
d = os.getcwd() d = os.getcwd()
while True: while True:
pf = os.path.join(d, ".v8-project.json") pf = os.path.join(d, ".v8-project.json")
@@ -87,9 +94,10 @@ V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP", "/ConfigurationRepositoryP"]
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"] IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
# --- Реквизиты хранилища из .v8-project.json --- # --- Запись базы в .v8-project.json ---
# Модель их не передаёт: скрипт сопоставляет параметры соединения с записью в databases[] # Модель не передаёт ни путь к платформе конкретной базы, ни реквизиты хранилища: скрипт
# и берёт repository оттуда. Тот же приём, что в cf-edit.py (сопоставление по configSrc). # сопоставляет параметры соединения с записью в databases[] и берёт их оттуда. Тот же приём,
# что в cf-edit.py (сопоставление по configSrc).
def _sg_find_v8project(start_dir): def _sg_find_v8project(start_dir):
d = start_dir d = start_dir
for _ in range(20): for _ in range(20):
@@ -328,10 +336,10 @@ def _version_key(p):
return [int(x) for x in re.findall(r"\d+", _version_dir(p))] return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
def resolve_v8path(v8path): def resolve_v8path(v8path, args):
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly).""" """Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path: if not v8path:
v8path = _find_project_v8path() v8path = _find_project_v8path(args)
if not v8path: if not v8path:
if os.name == "nt": if os.name == "nt":
candidates = ( candidates = (
@@ -542,7 +550,7 @@ def main():
args.ConfigDir = clean_path(args.ConfigDir, "-ConfigDir") args.ConfigDir = clean_path(args.ConfigDir, "-ConfigDir")
# --- Resolve V8Path --- # --- Resolve V8Path ---
v8path = resolve_v8path(args.V8Path) v8path = resolve_v8path(args.V8Path, args)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8" engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Resolve additional arguments for the selected engine --- # --- Resolve additional arguments for the selected engine ---
@@ -36,6 +36,7 @@ allowed-tools:
"name": "Разработка", "name": "Разработка",
"type": "file", "type": "file",
"path": "C:\\Bases\\MyApp_Dev", "path": "C:\\Bases\\MyApp_Dev",
"v8path": "C:\\Program Files\\1cv8\\8.3.27.1688\\bin",
"user": "Admin", "user": "Admin",
"password": "", "password": "",
"aliases": ["dev", "разработка"], "aliases": ["dev", "разработка"],
@@ -92,6 +93,7 @@ allowed-tools:
| `path` | string | для file | Путь к каталогу файловой базы | | `path` | string | для file | Путь к каталогу файловой базы |
| `server` | string | для server | Адрес сервера 1С | | `server` | string | для server | Адрес сервера 1С |
| `ref` | string | для server | Имя базы на сервере | | `ref` | string | для server | Имя базы на сервере |
| `v8path` | string | нет | Платформа именно этой базы — перекрывает корневой `v8path` (см. ниже) |
| `user` | string | нет | Имя пользователя 1С | | `user` | string | нет | Имя пользователя 1С |
| `password` | string | нет | Пароль | | `password` | string | нет | Пароль |
| `aliases` | string[] | нет | Альтернативные имена для быстрого доступа | | `aliases` | string[] | нет | Альтернативные имена для быстрого доступа |
@@ -145,9 +147,15 @@ allowed-tools:
После выполнения: если использованная база не зарегистрирована — предложи добавить через `/db-list add`. После выполнения: если использованная база не зарегистрирована — предложи добавить через `/db-list add`.
### Автоопределение платформы ### Выбор платформы
Если `v8path` не задан в конфиге: Порядок: явный `-V8Path` в вызове → `v8path` записи базы → корневой `v8path` → автоопределение.
Запись базы находит сам скрипт, сопоставляя параметры соединения с `databases[]`, — передавать
`-V8Path` руками не нужно. Поле у базы нужно, когда базы проекта живут на разных версиях платформы:
формат XML-выгрузки задаёт та платформа, которая выгружает, а автоопределение берёт самую старшую
установленную.
Если `v8path` не задан нигде:
```powershell ```powershell
$v8 = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort-Object -Descending | Select-Object -First 1 $v8 = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort-Object -Descending | Select-Object -First 1
@@ -24,19 +24,19 @@ allowed-tools:
## Параметры подключения ## Параметры подключения
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: Прочитай `.v8-project.json` из корня проекта и разреши базу:
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую 1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` 2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default` 4. Если ветка не совпала — используй `default`
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files). Платформа — `v8path` найденной записи базы, иначе корневой `v8path`. Не задан ни там, ни там — `-V8Path` не передавай.
Если файла нет — предложи `/db-list add`. Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" <параметры> python ".roo/skills/db-load-cf/scripts/db-load-cf.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -66,11 +66,11 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" <п
```powershell ```powershell
# Файловая база # Файловая база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\config.cf" python ".roo/skills/db-load-cf/scripts/db-load-cf.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\config.cf"
# Серверная база # Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "config.cf" python ".roo/skills/db-load-cf/scripts/db-load-cf.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "config.cf"
# Загрузка расширения # Загрузка расширения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "ext.cfe" -Extension "МоёРасширение" python ".roo/skills/db-load-cf/scripts/db-load-cf.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "ext.cfe" -Extension "МоёРасширение"
``` ```
@@ -1,4 +1,4 @@
# db-load-cf v1.17 — Load 1C configuration from CF file # db-load-cf v1.19 — Load 1C configuration from CF file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
@@ -299,8 +299,52 @@ function Assert-InfoBaseExists {
Assert-InfoBaseExists $InfoBasePath Assert-InfoBaseExists $InfoBasePath
# --- Запись базы в .v8-project.json ---
# Модель не передаёт ни путь к платформе конкретной базы, ни реквизиты хранилища: скрипт
# сопоставляет параметры соединения с записью в databases[] и берёт их оттуда. Тот же приём,
# что в cf-edit.ps1 (сопоставление по configSrc).
function Find-V8Project([string]$startDir) {
$d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) {
$pj = Join-Path $d ".v8-project.json"
if (Test-Path $pj) { return $pj }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
return $null
}
function Test-SamePath {
param([string]$A, [string]$B)
if (-not $A -or -not $B) { return $false }
try {
$na = [System.IO.Path]::GetFullPath($A).TrimEnd('\', '/')
$nb = [System.IO.Path]::GetFullPath($B).TrimEnd('\', '/')
return $na.Equals($nb, [System.StringComparison]::OrdinalIgnoreCase)
} catch { return $false }
}
function Find-ProjectDatabase {
# Запись базы в реестре, соответствующая переданному соединению. $null, если не найдена.
$pf = Find-V8Project (Get-Location).Path
if (-not $pf) { return $null }
try { $proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json } catch { return $null }
if (-not $proj.databases) { return $null }
foreach ($db in $proj.databases) {
if ($InfoBasePath -and $db.path -and (Test-SamePath $db.path $InfoBasePath)) { return $db }
if ($InfoBaseServer -and $InfoBaseRef -and $db.server -and $db.ref) {
if ($db.server.Equals($InfoBaseServer, [System.StringComparison]::OrdinalIgnoreCase) -and
$db.ref.Equals($InfoBaseRef, [System.StringComparison]::OrdinalIgnoreCase)) { return $db }
}
}
return $null
}
# --- Resolve V8Path --- # --- Resolve V8Path ---
function Find-ProjectV8Path { function Find-ProjectV8Path {
# v8path записи базы сильнее корневого: в одном проекте базы живут на разных версиях
# платформы, а версию формата выгрузки задаёт та платформа, которая выгружает.
$dbRec = Find-ProjectDatabase
if ($dbRec -and $dbRec.v8path) { return [string]$dbRec.v8path }
$dir = (Get-Location).Path $dir = (Get-Location).Path
while ($dir) { while ($dir) {
$pf = Join-Path $dir ".v8-project.json" $pf = Join-Path $dir ".v8-project.json"
@@ -413,18 +457,6 @@ function Write-PlatformOutput {
} }
function Find-V8Project([string]$startDir) {
$d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) {
$pj = Join-Path $d ".v8-project.json"
if (Test-Path $pj) { return $pj }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
return $null
}
# Постусловие применимости расширения: платформа отчитывается успехом и о расширении, которое # Постусловие применимости расширения: платформа отчитывается успехом и о расширении, которое
# не применит — отказ всплывает лениво, при первом вызове метода, записью в журнал регистрации. # не применит — отказ всплывает лениво, при первом вызове метода, записью в журнал регистрации.
# #
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-load-cf v1.17 — Load 1C configuration from CF file # db-load-cf v1.19 — Load 1C configuration from CF file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -37,8 +37,48 @@ def ci_parse_args(parser, argv=None):
def _find_project_v8path(): # --- Запись базы в .v8-project.json ---
"""Walk up from CWD to find .v8-project.json and read its v8path.""" # Модель не передаёт ни путь к платформе конкретной базы, ни реквизиты хранилища: скрипт
# сопоставляет параметры соединения с записью в databases[] и берёт их оттуда. Тот же приём,
# что в cf-edit.py (сопоставление по configSrc).
def same_path(a, b):
if not a or not b:
return False
try:
return os.path.abspath(a).rstrip("\\/").lower() == os.path.abspath(b).rstrip("\\/").lower()
except Exception:
return False
def find_project_database(args):
"""Запись базы в реестре, соответствующая переданному соединению. None, если не найдена."""
pf = _sg_find_v8project(os.getcwd())
if not pf:
return None
try:
with open(pf, encoding="utf-8-sig") as f:
proj = json.load(f)
except Exception:
return None
for db in proj.get("databases") or []:
if args.InfoBasePath and db.get("path") and same_path(db["path"], args.InfoBasePath):
return db
if args.InfoBaseServer and args.InfoBaseRef and db.get("server") and db.get("ref"):
if (db["server"].lower() == args.InfoBaseServer.lower()
and db["ref"].lower() == args.InfoBaseRef.lower()):
return db
return None
def _find_project_v8path(args):
"""Walk up from CWD to find .v8-project.json and read its v8path.
v8path записи базы сильнее корневого: в одном проекте базы живут на разных версиях
платформы, а версию формата выгрузки задаёт та платформа, которая выгружает.
"""
db = find_project_database(args)
if db and db.get("v8path"):
return db["v8path"]
d = os.getcwd() d = os.getcwd()
while True: while True:
pf = os.path.join(d, ".v8-project.json") pf = os.path.join(d, ".v8-project.json")
@@ -243,10 +283,10 @@ def _version_key(p):
return [int(x) for x in re.findall(r"\d+", _version_dir(p))] return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
def resolve_v8path(v8path): def resolve_v8path(v8path, args):
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly).""" """Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path: if not v8path:
v8path = _find_project_v8path() v8path = _find_project_v8path(args)
if not v8path: if not v8path:
if os.name == "nt": if os.name == "nt":
candidates = ( candidates = (
@@ -548,7 +588,7 @@ def main():
assert_infobase_exists(args.InfoBasePath) assert_infobase_exists(args.InfoBasePath)
args.InputFile = clean_path(args.InputFile, "-InputFile") args.InputFile = clean_path(args.InputFile, "-InputFile")
v8path = resolve_v8path(args.V8Path) v8path = resolve_v8path(args.V8Path, args)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8" engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Resolve additional arguments for the selected engine --- # --- Resolve additional arguments for the selected engine ---
@@ -40,19 +40,19 @@ allowed-tools:
## Параметры подключения ## Параметры подключения
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: Прочитай `.v8-project.json` из корня проекта и разреши базу:
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую 1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` 2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default` 4. Если ветка не совпала — используй `default`
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files). Платформа — `v8path` найденной записи базы, иначе корневой `v8path`. Не задан ни там, ни там — `-V8Path` не передавай.
Если файла нет — предложи `/db-list add`. Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" <параметры> python ".roo/skills/db-load-dt/scripts/db-load-dt.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -82,10 +82,10 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" <п
```powershell ```powershell
# Файловая база # Файловая база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\base.dt" python ".roo/skills/db-load-dt/scripts/db-load-dt.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\base.dt"
# Серверная база с ускорением загрузки # Серверная база с ускорением загрузки
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "base.dt" -JobsCount 4 python ".roo/skills/db-load-dt/scripts/db-load-dt.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "base.dt" -JobsCount 4
``` ```
## Связанные навыки ## Связанные навыки
@@ -1,4 +1,4 @@
# db-load-dt v1.16 — Load 1C information base from DT file # db-load-dt v1.17 — Load 1C information base from DT file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
@@ -286,8 +286,52 @@ function Assert-InfoBaseExists {
Assert-InfoBaseExists $InfoBasePath Assert-InfoBaseExists $InfoBasePath
# --- Запись базы в .v8-project.json ---
# Модель не передаёт ни путь к платформе конкретной базы, ни реквизиты хранилища: скрипт
# сопоставляет параметры соединения с записью в databases[] и берёт их оттуда. Тот же приём,
# что в cf-edit.ps1 (сопоставление по configSrc).
function Find-V8Project([string]$startDir) {
$d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) {
$pj = Join-Path $d ".v8-project.json"
if (Test-Path $pj) { return $pj }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
return $null
}
function Test-SamePath {
param([string]$A, [string]$B)
if (-not $A -or -not $B) { return $false }
try {
$na = [System.IO.Path]::GetFullPath($A).TrimEnd('\', '/')
$nb = [System.IO.Path]::GetFullPath($B).TrimEnd('\', '/')
return $na.Equals($nb, [System.StringComparison]::OrdinalIgnoreCase)
} catch { return $false }
}
function Find-ProjectDatabase {
# Запись базы в реестре, соответствующая переданному соединению. $null, если не найдена.
$pf = Find-V8Project (Get-Location).Path
if (-not $pf) { return $null }
try { $proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json } catch { return $null }
if (-not $proj.databases) { return $null }
foreach ($db in $proj.databases) {
if ($InfoBasePath -and $db.path -and (Test-SamePath $db.path $InfoBasePath)) { return $db }
if ($InfoBaseServer -and $InfoBaseRef -and $db.server -and $db.ref) {
if ($db.server.Equals($InfoBaseServer, [System.StringComparison]::OrdinalIgnoreCase) -and
$db.ref.Equals($InfoBaseRef, [System.StringComparison]::OrdinalIgnoreCase)) { return $db }
}
}
return $null
}
# --- Resolve V8Path --- # --- Resolve V8Path ---
function Find-ProjectV8Path { function Find-ProjectV8Path {
# v8path записи базы сильнее корневого: в одном проекте базы живут на разных версиях
# платформы, а версию формата выгрузки задаёт та платформа, которая выгружает.
$dbRec = Find-ProjectDatabase
if ($dbRec -and $dbRec.v8path) { return [string]$dbRec.v8path }
$dir = (Get-Location).Path $dir = (Get-Location).Path
while ($dir) { while ($dir) {
$pf = Join-Path $dir ".v8-project.json" $pf = Join-Path $dir ".v8-project.json"
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-load-dt v1.16 — Load 1C information base from DT file # db-load-dt v1.17 — Load 1C information base from DT file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -37,8 +37,62 @@ def ci_parse_args(parser, argv=None):
def _find_project_v8path(): # --- Запись базы в .v8-project.json ---
"""Walk up from CWD to find .v8-project.json and read its v8path.""" # Модель не передаёт ни путь к платформе конкретной базы, ни реквизиты хранилища: скрипт
# сопоставляет параметры соединения с записью в databases[] и берёт их оттуда. Тот же приём,
# что в cf-edit.py (сопоставление по configSrc).
def _sg_find_v8project(start_dir):
d = start_dir
for _ in range(20):
if not d:
break
pj = os.path.join(d, ".v8-project.json")
if os.path.isfile(pj):
return pj
parent = os.path.dirname(d)
if parent == d:
break
d = parent
return None
def same_path(a, b):
if not a or not b:
return False
try:
return os.path.abspath(a).rstrip("\\/").lower() == os.path.abspath(b).rstrip("\\/").lower()
except Exception:
return False
def find_project_database(args):
"""Запись базы в реестре, соответствующая переданному соединению. None, если не найдена."""
pf = _sg_find_v8project(os.getcwd())
if not pf:
return None
try:
with open(pf, encoding="utf-8-sig") as f:
proj = json.load(f)
except Exception:
return None
for db in proj.get("databases") or []:
if args.InfoBasePath and db.get("path") and same_path(db["path"], args.InfoBasePath):
return db
if args.InfoBaseServer and args.InfoBaseRef and db.get("server") and db.get("ref"):
if (db["server"].lower() == args.InfoBaseServer.lower()
and db["ref"].lower() == args.InfoBaseRef.lower()):
return db
return None
def _find_project_v8path(args):
"""Walk up from CWD to find .v8-project.json and read its v8path.
v8path записи базы сильнее корневого: в одном проекте базы живут на разных версиях
платформы, а версию формата выгрузки задаёт та платформа, которая выгружает.
"""
db = find_project_database(args)
if db and db.get("v8path"):
return db["v8path"]
d = os.getcwd() d = os.getcwd()
while True: while True:
pf = os.path.join(d, ".v8-project.json") pf = os.path.join(d, ".v8-project.json")
@@ -243,10 +297,10 @@ def _version_key(p):
return [int(x) for x in re.findall(r"\d+", _version_dir(p))] return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
def resolve_v8path(v8path): def resolve_v8path(v8path, args):
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly).""" """Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path: if not v8path:
v8path = _find_project_v8path() v8path = _find_project_v8path(args)
if not v8path: if not v8path:
if os.name == "nt": if os.name == "nt":
candidates = ( candidates = (
@@ -457,7 +511,7 @@ def main():
assert_infobase_exists(args.InfoBasePath) assert_infobase_exists(args.InfoBasePath)
args.InputFile = clean_path(args.InputFile, "-InputFile") args.InputFile = clean_path(args.InputFile, "-InputFile")
v8path = resolve_v8path(args.V8Path) v8path = resolve_v8path(args.V8Path, args)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8" engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Resolve additional arguments for the selected engine --- # --- Resolve additional arguments for the selected engine ---
@@ -25,12 +25,12 @@ allowed-tools:
## Параметры подключения ## Параметры подключения
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: Прочитай `.v8-project.json` из корня проекта и разреши базу:
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую 1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` 2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default` 4. Если ветка не совпала — используй `default`
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files). Платформа — `v8path` найденной записи базы, иначе корневой `v8path`. Не задан ни там, ни там — `-V8Path` не передавай.
Если файла нет — предложи `/db-list add`. Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
Если в записи базы указан `configSrc` — используй как каталог конфигурации. Если в записи базы указан `configSrc` — используй как каталог конфигурации.
@@ -38,7 +38,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" <параметры> python ".roo/skills/db-load-git/scripts/db-load-git.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -73,8 +73,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" <
```powershell ```powershell
# Все незафиксированные изменения # Все незафиксированные изменения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source All -UpdateDB python ".roo/skills/db-load-git/scripts/db-load-git.py" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source All -UpdateDB
# Из диапазона коммитов # Из диапазона коммитов
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source Commit -CommitRange "HEAD~3..HEAD" python ".roo/skills/db-load-git/scripts/db-load-git.py" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source Commit -CommitRange "HEAD~3..HEAD"
``` ```
@@ -1,4 +1,4 @@
# db-load-git v1.26 — Load Git changes into 1C database # db-load-git v1.27 — Load Git changes into 1C database
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
@@ -139,9 +139,10 @@ param(
$OutputEncoding = [System.Text.Encoding]::UTF8 $OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Реквизиты хранилища из .v8-project.json --- # --- Запись базы в .v8-project.json ---
# Модель их не передаёт: скрипт сопоставляет параметры соединения с записью в databases[] # Модель не передаёт ни путь к платформе конкретной базы, ни реквизиты хранилища: скрипт
# и берёт repository оттуда. Тот же приём, что в cf-edit.ps1 (сопоставление по configSrc). # сопоставляет параметры соединения с записью в databases[] и берёт их оттуда. Тот же приём,
# что в cf-edit.ps1 (сопоставление по configSrc).
function Find-V8Project([string]$startDir) { function Find-V8Project([string]$startDir) {
$d = $startDir $d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) { for ($i = 0; $i -lt 20 -and $d; $i++) {
@@ -427,6 +428,10 @@ function Get-ObjectXmlFromSubFile {
# --- Resolve V8Path (skip if DryRun) --- # --- Resolve V8Path (skip if DryRun) ---
if (-not $DryRun) { if (-not $DryRun) {
function Find-ProjectV8Path { function Find-ProjectV8Path {
# v8path записи базы сильнее корневого: в одном проекте базы живут на разных версиях
# платформы, а версию формата выгрузки задаёт та платформа, которая выгружает.
$dbRec = Find-ProjectDatabase
if ($dbRec -and $dbRec.v8path) { return [string]$dbRec.v8path }
$dir = (Get-Location).Path $dir = (Get-Location).Path
while ($dir) { while ($dir) {
$pf = Join-Path $dir ".v8-project.json" $pf = Join-Path $dir ".v8-project.json"
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-load-git v1.26 — Load Git changes into 1C database # db-load-git v1.27 — Load Git changes into 1C database
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -37,8 +37,15 @@ def ci_parse_args(parser, argv=None):
def _find_project_v8path(): def _find_project_v8path(args):
"""Walk up from CWD to find .v8-project.json and read its v8path.""" """Walk up from CWD to find .v8-project.json and read its v8path.
v8path записи базы сильнее корневого: в одном проекте базы живут на разных версиях
платформы, а версию формата выгрузки задаёт та платформа, которая выгружает.
"""
db = find_project_database(args)
if db and db.get("v8path"):
return db["v8path"]
d = os.getcwd() d = os.getcwd()
while True: while True:
pf = os.path.join(d, ".v8-project.json") pf = os.path.join(d, ".v8-project.json")
@@ -87,9 +94,10 @@ V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP", "/ConfigurationRepositoryP"]
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"] IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
# --- Реквизиты хранилища из .v8-project.json --- # --- Запись базы в .v8-project.json ---
# Модель их не передаёт: скрипт сопоставляет параметры соединения с записью в databases[] # Модель не передаёт ни путь к платформе конкретной базы, ни реквизиты хранилища: скрипт
# и берёт repository оттуда. Тот же приём, что в cf-edit.py (сопоставление по configSrc). # сопоставляет параметры соединения с записью в databases[] и берёт их оттуда. Тот же приём,
# что в cf-edit.py (сопоставление по configSrc).
def _sg_find_v8project(start_dir): def _sg_find_v8project(start_dir):
d = start_dir d = start_dir
for _ in range(20): for _ in range(20):
@@ -349,10 +357,10 @@ def _version_key(p):
return [int(x) for x in re.findall(r"\d+", _version_dir(p))] return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
def resolve_v8path(v8path): def resolve_v8path(v8path, args):
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly).""" """Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path: if not v8path:
v8path = _find_project_v8path() v8path = _find_project_v8path(args)
if not v8path: if not v8path:
if os.name == "nt": if os.name == "nt":
candidates = ( candidates = (
@@ -714,7 +722,7 @@ def main():
# --- Resolve V8Path (skip if DryRun) --- # --- Resolve V8Path (skip if DryRun) ---
v8path = None v8path = None
if not args.DryRun: if not args.DryRun:
v8path = resolve_v8path(args.V8Path) v8path = resolve_v8path(args.V8Path, args)
# --- Detect engine + validate connection (skip if DryRun) --- # --- Detect engine + validate connection (skip if DryRun) ---
engine = "1cv8" engine = "1cv8"
@@ -25,12 +25,12 @@ allowed-tools:
## Параметры подключения ## Параметры подключения
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: Прочитай `.v8-project.json` из корня проекта и разреши базу:
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую 1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` 2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default` 4. Если ветка не совпала — используй `default`
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files). Платформа — `v8path` найденной записи базы, иначе корневой `v8path`. Не задан ни там, ни там — `-V8Path` не передавай.
Если файла нет — предложи `/db-list add`. Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
Если в записи базы указан `configSrc` — используй как каталог загрузки по умолчанию. Если в записи базы указан `configSrc` — используй как каталог загрузки по умолчанию.
@@ -39,7 +39,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" <параметры> python ".roo/skills/db-load-xml/scripts/db-load-xml.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -92,14 +92,14 @@ Documents/Заказ/Forms/ФормаДокумента.xml
```powershell ```powershell
# Полная загрузка # Полная загрузка
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full python ".roo/skills/db-load-xml/scripts/db-load-xml.py" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full
# Частичная загрузка конкретных файлов # Частичная загрузка конкретных файлов
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl" python ".roo/skills/db-load-xml/scripts/db-load-xml.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl"
# Загрузка расширения # Загрузка расширения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение" python ".roo/skills/db-load-xml/scripts/db-load-xml.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
# Загрузка + обновление БД в одном запуске # Загрузка + обновление БД в одном запуске
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full -UpdateDB python ".roo/skills/db-load-xml/scripts/db-load-xml.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full -UpdateDB
``` ```
@@ -1,4 +1,4 @@
# db-load-xml v1.28 — Load 1C configuration from XML files # db-load-xml v1.29 — Load 1C configuration from XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
@@ -135,9 +135,10 @@ param(
$OutputEncoding = [System.Text.Encoding]::UTF8 $OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Реквизиты хранилища из .v8-project.json --- # --- Запись базы в .v8-project.json ---
# Модель их не передаёт: скрипт сопоставляет параметры соединения с записью в databases[] # Модель не передаёт ни путь к платформе конкретной базы, ни реквизиты хранилища: скрипт
# и берёт repository оттуда. Тот же приём, что в cf-edit.ps1 (сопоставление по configSrc). # сопоставляет параметры соединения с записью в databases[] и берёт их оттуда. Тот же приём,
# что в cf-edit.ps1 (сопоставление по configSrc).
function Find-V8Project([string]$startDir) { function Find-V8Project([string]$startDir) {
$d = $startDir $d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) { for ($i = 0; $i -lt 20 -and $d; $i++) {
@@ -448,6 +449,10 @@ Assert-InfoBaseExists $InfoBasePath
# --- Resolve V8Path --- # --- Resolve V8Path ---
function Find-ProjectV8Path { function Find-ProjectV8Path {
# v8path записи базы сильнее корневого: в одном проекте базы живут на разных версиях
# платформы, а версию формата выгрузки задаёт та платформа, которая выгружает.
$dbRec = Find-ProjectDatabase
if ($dbRec -and $dbRec.v8path) { return [string]$dbRec.v8path }
$dir = (Get-Location).Path $dir = (Get-Location).Path
while ($dir) { while ($dir) {
$pf = Join-Path $dir ".v8-project.json" $pf = Join-Path $dir ".v8-project.json"
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-load-xml v1.28 — Load 1C configuration from XML files # db-load-xml v1.29 — Load 1C configuration from XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -37,8 +37,15 @@ def ci_parse_args(parser, argv=None):
def _find_project_v8path(): def _find_project_v8path(args):
"""Walk up from CWD to find .v8-project.json and read its v8path.""" """Walk up from CWD to find .v8-project.json and read its v8path.
v8path записи базы сильнее корневого: в одном проекте базы живут на разных версиях
платформы, а версию формата выгрузки задаёт та платформа, которая выгружает.
"""
db = find_project_database(args)
if db and db.get("v8path"):
return db["v8path"]
d = os.getcwd() d = os.getcwd()
while True: while True:
pf = os.path.join(d, ".v8-project.json") pf = os.path.join(d, ".v8-project.json")
@@ -87,9 +94,10 @@ V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP", "/ConfigurationRepositoryP"]
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"] IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
# --- Реквизиты хранилища из .v8-project.json --- # --- Запись базы в .v8-project.json ---
# Модель их не передаёт: скрипт сопоставляет параметры соединения с записью в databases[] # Модель не передаёт ни путь к платформе конкретной базы, ни реквизиты хранилища: скрипт
# и берёт repository оттуда. Тот же приём, что в cf-edit.py (сопоставление по configSrc). # сопоставляет параметры соединения с записью в databases[] и берёт их оттуда. Тот же приём,
# что в cf-edit.py (сопоставление по configSrc).
def _sg_find_v8project(start_dir): def _sg_find_v8project(start_dir):
d = start_dir d = start_dir
for _ in range(20): for _ in range(20):
@@ -349,10 +357,10 @@ def _version_key(p):
return [int(x) for x in re.findall(r"\d+", _version_dir(p))] return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
def resolve_v8path(v8path): def resolve_v8path(v8path, args):
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly).""" """Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path: if not v8path:
v8path = _find_project_v8path() v8path = _find_project_v8path(args)
if not v8path: if not v8path:
if os.name == "nt": if os.name == "nt":
candidates = ( candidates = (
@@ -692,7 +700,7 @@ def main():
args.ListFile = clean_path(args.ListFile, "-ListFile") args.ListFile = clean_path(args.ListFile, "-ListFile")
# --- Resolve V8Path --- # --- Resolve V8Path ---
v8path = resolve_v8path(args.V8Path) v8path = resolve_v8path(args.V8Path, args)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8" engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
@@ -63,12 +63,14 @@ allowed-tools:
## Параметры подключения ## Параметры подключения
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу: Прочитай `.v8-project.json` из корня проекта и разреши базу:
1. Если пользователь указал параметры подключения — используй напрямую 1. Если пользователь указал параметры подключения — используй напрямую
2. Если указал базу по имени — ищи по id / alias / name 2. Если указал базу по имени — ищи по id / alias / name
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default` 4. Если ветка не совпала — используй `default`
Платформа — `v8path` найденной записи базы, иначе корневой `v8path`. Не задан ни там, ни там — `-V8Path` не передавай.
Реквизиты хранилища передавать не нужно: запись базы находится по переданным параметрам Реквизиты хранилища передавать не нужно: запись базы находится по переданным параметрам
соединения (`-InfoBasePath` либо `-InfoBaseServer` + `-InfoBaseRef`), реквизиты берутся из её соединения (`-InfoBasePath` либо `-InfoBaseServer` + `-InfoBaseRef`), реквизиты берутся из её
`repository`. Задать их явно можно параметрами `-Repository*`. `repository`. Задать их явно можно параметрами `-Repository*`.
@@ -76,7 +78,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command <подкоманда> <параметры> python ".roo/skills/db-repo/scripts/db-repo.py" -Command <подкоманда> <параметры>
``` ```
### Рабочий цикл ### Рабочий цикл
@@ -174,22 +176,22 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Comma
```powershell ```powershell
# Захватить справочник вместе с подчинёнными объектами # Захватить справочник вместе с подчинёнными объектами
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command lock -InfoBasePath "C:\Bases\MyDB" -Objects "Справочник.Номенклатура" -WithChildren python ".roo/skills/db-repo/scripts/db-repo.py" -Command lock -InfoBasePath "C:\Bases\MyDB" -Objects "Справочник.Номенклатура" -WithChildren
# Захватить корень — он нужен, чтобы добавить или удалить объект конфигурации # Захватить корень — он нужен, чтобы добавить или удалить объект конфигурации
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command lock -InfoBasePath "C:\Bases\MyDB" -Objects "Конфигурация" python ".roo/skills/db-repo/scripts/db-repo.py" -Command lock -InfoBasePath "C:\Bases\MyDB" -Objects "Конфигурация"
# Поместить новый объект: он уже существует, поэтому называется вместе с корнем # Поместить новый объект: он уже существует, поэтому называется вместе с корнем
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command commit -InfoBasePath "C:\Bases\MyDB" -Objects "Конфигурация,Справочник.Склады" -Comment "Добавлен справочник Склады" python ".roo/skills/db-repo/scripts/db-repo.py" -Command commit -InfoBasePath "C:\Bases\MyDB" -Objects "Конфигурация,Справочник.Склады" -Comment "Добавлен справочник Склады"
# Поместить с комментарием, оставив захват # Поместить с комментарием, оставив захват
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command commit -InfoBasePath "C:\Bases\MyDB" -Objects "Справочник.Номенклатура" -Comment "Добавлен реквизит Артикул" -KeepLocked python ".roo/skills/db-repo/scripts/db-repo.py" -Command commit -InfoBasePath "C:\Bases\MyDB" -Objects "Справочник.Номенклатура" -Comment "Добавлен реквизит Артикул" -KeepLocked
# Получить изменения из хранилища # Получить изменения из хранилища
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command update -InfoBasePath "C:\Bases\MyDB" python ".roo/skills/db-repo/scripts/db-repo.py" -Command update -InfoBasePath "C:\Bases\MyDB"
# Серверная база, расширение # Серверная база, расширение
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command lock -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Extension "МоёРасширение" -Objects "Справочник.Номенклатура" python ".roo/skills/db-repo/scripts/db-repo.py" -Command lock -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Extension "МоёРасширение" -Objects "Справочник.Номенклатура"
``` ```
## После выполнения ## После выполнения
@@ -3,7 +3,7 @@
## create — создать хранилище ## create — создать хранилище
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command create -InfoBasePath "C:\Bases\MyDB" -RepositoryPath "C:\Repo\MyApp" -RepositoryUser "Admin" -RepositoryPassword "…" python ".roo/skills/db-repo/scripts/db-repo.py" -Command create -InfoBasePath "C:\Bases\MyDB" -RepositoryPath "C:\Repo\MyApp" -RepositoryUser "Admin" -RepositoryPassword "…"
``` ```
| Параметр | Описание | | Параметр | Описание |
@@ -22,7 +22,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Comma
## add-user — создать пользователя ## add-user — создать пользователя
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command add-user -InfoBasePath "C:\Bases\MyDB" -NewUser "Ivanov" -NewUserPassword "…" -Rights LockObjects python ".roo/skills/db-repo/scripts/db-repo.py" -Command add-user -InfoBasePath "C:\Bases\MyDB" -NewUser "Ivanov" -NewUserPassword "…" -Rights LockObjects
``` ```
| Право | Что даёт | | Право | Что даёт |
@@ -38,7 +38,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Comma
## copy-users — скопировать пользователей из другого хранилища ## copy-users — скопировать пользователей из другого хранилища
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command copy-users -InfoBasePath "C:\Bases\MyDB" -SourcePath "\\srv01\repo\Other" -SourceUser "Admin" -SourcePassword "…" python ".roo/skills/db-repo/scripts/db-repo.py" -Command copy-users -InfoBasePath "C:\Bases\MyDB" -SourcePath "\\srv01\repo\Other" -SourceUser "Admin" -SourcePassword "…"
``` ```
`-SourcePath`, `-SourceUser`, `-SourcePassword` описывают хранилище-**источник**. Удалённые пользователи `-SourcePath`, `-SourceUser`, `-SourcePassword` описывают хранилище-**источник**. Удалённые пользователи
@@ -3,7 +3,7 @@
## connect — подключить ## connect — подключить
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command connect -InfoBasePath "C:\Bases\MyDB" -RepositoryPath "\\srv01\repo\MyApp" -RepositoryUser "Ivanov" -RepositoryPassword "…" python ".roo/skills/db-repo/scripts/db-repo.py" -Command connect -InfoBasePath "C:\Bases\MyDB" -RepositoryPath "\\srv01\repo\MyApp" -RepositoryUser "Ivanov" -RepositoryPassword "…"
``` ```
| Параметр | Описание | | Параметр | Описание |
@@ -23,7 +23,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Comma
## disconnect — отключить ## disconnect — отключить
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command disconnect -InfoBasePath "C:\Bases\MyDB" python ".roo/skills/db-repo/scripts/db-repo.py" -Command disconnect -InfoBasePath "C:\Bases\MyDB"
``` ```
**Спроси подтверждение у пользователя.** Отключение снимает связь и на стороне самого хранилища: **Спроси подтверждение у пользователя.** Отключение снимает связь и на стороне самого хранилища:
@@ -3,7 +3,7 @@
## report — отчёт по версиям ## report — отчёт по версиям
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command report -InfoBasePath "C:\Bases\MyDB" -OutputFile "C:\tmp\repo.txt" python ".roo/skills/db-repo/scripts/db-repo.py" -Command report -InfoBasePath "C:\Bases\MyDB" -OutputFile "C:\tmp\repo.txt"
``` ```
| Параметр | Описание | | Параметр | Описание |
@@ -25,7 +25,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Comma
## dump-cfg — выгрузить версию в CF ## dump-cfg — выгрузить версию в CF
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command dump-cfg -InfoBasePath "C:\Bases\MyDB" -OutputFile "C:\tmp\v120.cf" -Version 120 python ".roo/skills/db-repo/scripts/db-repo.py" -Command dump-cfg -InfoBasePath "C:\Bases\MyDB" -OutputFile "C:\tmp\v120.cf" -Version 120
``` ```
Без `-Version` (или при `-1`) выгружается последняя версия. Без `-Version` (или при `-1`) выгружается последняя версия.
@@ -3,7 +3,7 @@
## set-label — метка на версию ## set-label — метка на версию
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command set-label -InfoBasePath "C:\Bases\MyDB" -Label "Релиз 1.2" -Version 120 -Comment "Передано в тест" python ".roo/skills/db-repo/scripts/db-repo.py" -Command set-label -InfoBasePath "C:\Bases\MyDB" -Label "Релиз 1.2" -Version 120 -Comment "Передано в тест"
``` ```
Без `-Version` метка ставится на последнюю версию. Несуществующая версия — ошибка. Без `-Version` метка ставится на последнюю версию. Несуществующая версия — ошибка.
@@ -11,7 +11,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Comma
## optimize — оптимизация хранения ## optimize — оптимизация хранения
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command optimize -InfoBasePath "C:\Bases\MyDB" python ".roo/skills/db-repo/scripts/db-repo.py" -Command optimize -InfoBasePath "C:\Bases\MyDB"
``` ```
Оптимизирует хранение данных в хранилище. Операция долгая. Оптимизирует хранение данных в хранилище. Операция долгая.
@@ -19,7 +19,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Comma
## clear-cache — очистка кеша ## clear-cache — очистка кеша
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command clear-cache -InfoBasePath "C:\Bases\MyDB" -CacheScope local python ".roo/skills/db-repo/scripts/db-repo.py" -Command clear-cache -InfoBasePath "C:\Bases\MyDB" -CacheScope local
``` ```
| `-CacheScope` | Что чистит | | `-CacheScope` | Что чистит |
@@ -1,4 +1,4 @@
# db-repo v1.14 — 1C configuration repository operations # db-repo v1.16 — 1C configuration repository operations
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: движок только 1cv8 — ibcmd работу с хранилищем не поддерживает (нет такого режима). # NB: движок только 1cv8 — ibcmd работу с хранилищем не поддерживает (нет такого режима).
<# <#
@@ -320,8 +320,52 @@ function Assert-InfoBaseExists {
Assert-InfoBaseExists $InfoBasePath Assert-InfoBaseExists $InfoBasePath
# --- Запись базы в .v8-project.json ---
# Модель не передаёт ни путь к платформе конкретной базы, ни реквизиты хранилища: скрипт
# сопоставляет параметры соединения с записью в databases[] и берёт их оттуда. Тот же приём,
# что в cf-edit.ps1 (сопоставление по configSrc).
function Find-V8Project([string]$startDir) {
$d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) {
$pj = Join-Path $d ".v8-project.json"
if (Test-Path $pj) { return $pj }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
return $null
}
function Test-SamePath {
param([string]$A, [string]$B)
if (-not $A -or -not $B) { return $false }
try {
$na = [System.IO.Path]::GetFullPath($A).TrimEnd('\', '/')
$nb = [System.IO.Path]::GetFullPath($B).TrimEnd('\', '/')
return $na.Equals($nb, [System.StringComparison]::OrdinalIgnoreCase)
} catch { return $false }
}
function Find-ProjectDatabase {
# Запись базы в реестре, соответствующая переданному соединению. $null, если не найдена.
$pf = Find-V8Project (Get-Location).Path
if (-not $pf) { return $null }
try { $proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json } catch { return $null }
if (-not $proj.databases) { return $null }
foreach ($db in $proj.databases) {
if ($InfoBasePath -and $db.path -and (Test-SamePath $db.path $InfoBasePath)) { return $db }
if ($InfoBaseServer -and $InfoBaseRef -and $db.server -and $db.ref) {
if ($db.server.Equals($InfoBaseServer, [System.StringComparison]::OrdinalIgnoreCase) -and
$db.ref.Equals($InfoBaseRef, [System.StringComparison]::OrdinalIgnoreCase)) { return $db }
}
}
return $null
}
# --- Resolve V8Path --- # --- Resolve V8Path ---
function Find-ProjectV8Path { function Find-ProjectV8Path {
# v8path записи базы сильнее корневого: в одном проекте базы живут на разных версиях
# платформы, а версию формата выгрузки задаёт та платформа, которая выгружает.
$dbRec = Find-ProjectDatabase
if ($dbRec -and $dbRec.v8path) { return [string]$dbRec.v8path }
$dir = (Get-Location).Path $dir = (Get-Location).Path
while ($dir) { while ($dir) {
$pf = Join-Path $dir ".v8-project.json" $pf = Join-Path $dir ".v8-project.json"
@@ -499,46 +543,6 @@ function Invoke-PlatformProcess {
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode } return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
} }
# --- Реквизиты хранилища из .v8-project.json ---
# Модель их не передаёт: скрипт сопоставляет параметры соединения с записью в databases[]
# и берёт repository оттуда. Тот же приём, что в cf-edit.ps1 (сопоставление по configSrc).
function Find-V8Project([string]$startDir) {
$d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) {
$pj = Join-Path $d ".v8-project.json"
if (Test-Path $pj) { return $pj }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
return $null
}
function Test-SamePath {
param([string]$A, [string]$B)
if (-not $A -or -not $B) { return $false }
try {
$na = [System.IO.Path]::GetFullPath($A).TrimEnd('\', '/')
$nb = [System.IO.Path]::GetFullPath($B).TrimEnd('\', '/')
return $na.Equals($nb, [System.StringComparison]::OrdinalIgnoreCase)
} catch { return $false }
}
function Find-ProjectDatabase {
# Запись базы в реестре, соответствующая переданному соединению. $null, если не найдена.
$pf = Find-V8Project (Get-Location).Path
if (-not $pf) { return $null }
try { $proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json } catch { return $null }
if (-not $proj.databases) { return $null }
foreach ($db in $proj.databases) {
if ($InfoBasePath -and $db.path -and (Test-SamePath $db.path $InfoBasePath)) { return $db }
if ($InfoBaseServer -and $InfoBaseRef -and $db.server -and $db.ref) {
if ($db.server.Equals($InfoBaseServer, [System.StringComparison]::OrdinalIgnoreCase) -and
$db.ref.Equals($InfoBaseRef, [System.StringComparison]::OrdinalIgnoreCase)) { return $db }
}
}
return $null
}
function Resolve-RepositorySettings { function Resolve-RepositorySettings {
# Возвращает @{ Path; User; Password; FromRegistry }. Явные -Repository* всегда сильнее реестра. # Возвращает @{ Path; User; Password; FromRegistry }. Явные -Repository* всегда сильнее реестра.
$dbRec = Find-ProjectDatabase $dbRec = Find-ProjectDatabase
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-repo v1.14 — 1C configuration repository operations # db-repo v1.16 — 1C configuration repository operations
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: движок только 1cv8 — ibcmd работу с хранилищем не поддерживает (нет такого режима). # NB: движок только 1cv8 — ibcmd работу с хранилищем не поддерживает (нет такого режима).
"""Работа с хранилищем конфигурации 1С. """Работа с хранилищем конфигурации 1С.
@@ -78,8 +78,15 @@ def ci_parse_args(parser, argv=None):
return parser.parse_args(argv) return parser.parse_args(argv)
def _find_project_v8path(): def _find_project_v8path(args):
"""Walk up from CWD to find .v8-project.json and read its v8path.""" """Walk up from CWD to find .v8-project.json and read its v8path.
v8path записи базы сильнее корневого: в одном проекте базы живут на разных версиях
платформы, а версию формата выгрузки задаёт та платформа, которая выгружает.
"""
db = find_project_database(args)
if db and db.get("v8path"):
return db["v8path"]
d = os.getcwd() d = os.getcwd()
while True: while True:
pf = os.path.join(d, ".v8-project.json") pf = os.path.join(d, ".v8-project.json")
@@ -224,10 +231,10 @@ def _version_key(p):
return [int(x) for x in re.findall(r"\d+", _version_dir(p))] return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
def resolve_v8path(v8path): def resolve_v8path(v8path, args):
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly).""" """Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path: if not v8path:
v8path = _find_project_v8path() v8path = _find_project_v8path(args)
if not v8path: if not v8path:
if os.name == "nt": if os.name == "nt":
candidates = ( candidates = (
@@ -405,9 +412,10 @@ def resolve_extra_args(v8_extra, hints):
return extra return extra
# --- Реквизиты хранилища из .v8-project.json --- # --- Запись базы в .v8-project.json ---
# Модель их не передаёт: скрипт сопоставляет параметры соединения с записью в databases[] # Модель не передаёт ни путь к платформе конкретной базы, ни реквизиты хранилища: скрипт
# и берёт repository оттуда. Тот же приём, что в cf-edit.py (сопоставление по configSrc). # сопоставляет параметры соединения с записью в databases[] и берёт их оттуда. Тот же приём,
# что в cf-edit.py (сопоставление по configSrc).
def _sg_find_v8project(start_dir): def _sg_find_v8project(start_dir):
d = start_dir d = start_dir
for _ in range(20): for _ in range(20):
@@ -983,7 +991,7 @@ def main():
args.V8Path = clean_path(args.V8Path, "-V8Path") args.V8Path = clean_path(args.V8Path, "-V8Path")
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath") args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
assert_infobase_exists(args.InfoBasePath) assert_infobase_exists(args.InfoBasePath)
v8path = resolve_v8path(args.V8Path) v8path = resolve_v8path(args.V8Path, args)
if not args.Command: if not args.Command:
print("Error: -Command is required. Known: %s" % ", ".join(sorted(COMMAND_KEYS))) print("Error: -Command is required. Known: %s" % ", ".join(sorted(COMMAND_KEYS)))
@@ -24,19 +24,19 @@ allowed-tools:
## Параметры подключения ## Параметры подключения
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: Прочитай `.v8-project.json` из корня проекта и разреши базу:
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую 1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` 2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default` 4. Если ветка не совпала — используй `default`
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files). Платформа — `v8path` найденной записи базы, иначе корневой `v8path`. Не задан ни там, ни там — `-V8Path` не передавай.
Если файла нет — предложи `/db-list add`. Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" <параметры> python ".roo/skills/db-run/scripts/db-run.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -64,14 +64,14 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" <пар
```powershell ```powershell
# Простой запуск # Простой запуск
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" python ".roo/skills/db-run/scripts/db-run.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin"
# Запуск с обработкой # Запуск с обработкой
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Execute "C:\epf\МояОбработка.epf" python ".roo/skills/db-run/scripts/db-run.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Execute "C:\epf\МояОбработка.epf"
# Открыть по навигационной ссылке # Открыть по навигационной ссылке
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -URL "e1cib/data/Справочник.Номенклатура" python ".roo/skills/db-run/scripts/db-run.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -URL "e1cib/data/Справочник.Номенклатура"
# Серверная база с параметром запуска # Серверная база с параметром запуска
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -CParam "ЗапуститьОбновление" python ".roo/skills/db-run/scripts/db-run.py" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -CParam "ЗапуститьОбновление"
``` ```
@@ -1,4 +1,4 @@
# db-run v1.10 — Launch 1C:Enterprise # db-run v1.11 — Launch 1C:Enterprise
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
@@ -262,8 +262,52 @@ $V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
$InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath' $InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath'
$Execute = ConvertTo-CleanPath $Execute '-Execute' $Execute = ConvertTo-CleanPath $Execute '-Execute'
# --- Запись базы в .v8-project.json ---
# Модель не передаёт ни путь к платформе конкретной базы, ни реквизиты хранилища: скрипт
# сопоставляет параметры соединения с записью в databases[] и берёт их оттуда. Тот же приём,
# что в cf-edit.ps1 (сопоставление по configSrc).
function Find-V8Project([string]$startDir) {
$d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) {
$pj = Join-Path $d ".v8-project.json"
if (Test-Path $pj) { return $pj }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
return $null
}
function Test-SamePath {
param([string]$A, [string]$B)
if (-not $A -or -not $B) { return $false }
try {
$na = [System.IO.Path]::GetFullPath($A).TrimEnd('\', '/')
$nb = [System.IO.Path]::GetFullPath($B).TrimEnd('\', '/')
return $na.Equals($nb, [System.StringComparison]::OrdinalIgnoreCase)
} catch { return $false }
}
function Find-ProjectDatabase {
# Запись базы в реестре, соответствующая переданному соединению. $null, если не найдена.
$pf = Find-V8Project (Get-Location).Path
if (-not $pf) { return $null }
try { $proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json } catch { return $null }
if (-not $proj.databases) { return $null }
foreach ($db in $proj.databases) {
if ($InfoBasePath -and $db.path -and (Test-SamePath $db.path $InfoBasePath)) { return $db }
if ($InfoBaseServer -and $InfoBaseRef -and $db.server -and $db.ref) {
if ($db.server.Equals($InfoBaseServer, [System.StringComparison]::OrdinalIgnoreCase) -and
$db.ref.Equals($InfoBaseRef, [System.StringComparison]::OrdinalIgnoreCase)) { return $db }
}
}
return $null
}
# --- Resolve V8Path --- # --- Resolve V8Path ---
function Find-ProjectV8Path { function Find-ProjectV8Path {
# v8path записи базы сильнее корневого: в одном проекте базы живут на разных версиях
# платформы, а версию формата выгрузки задаёт та платформа, которая выгружает.
$dbRec = Find-ProjectDatabase
if ($dbRec -and $dbRec.v8path) { return [string]$dbRec.v8path }
$dir = (Get-Location).Path $dir = (Get-Location).Path
while ($dir) { while ($dir) {
$pf = Join-Path $dir ".v8-project.json" $pf = Join-Path $dir ".v8-project.json"
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-run v1.10 — Launch 1C:Enterprise # db-run v1.11 — Launch 1C:Enterprise
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -34,8 +34,62 @@ def ci_parse_args(parser, argv=None):
def _find_project_v8path(): # --- Запись базы в .v8-project.json ---
"""Walk up from CWD to find .v8-project.json and read its v8path.""" # Модель не передаёт ни путь к платформе конкретной базы, ни реквизиты хранилища: скрипт
# сопоставляет параметры соединения с записью в databases[] и берёт их оттуда. Тот же приём,
# что в cf-edit.py (сопоставление по configSrc).
def _sg_find_v8project(start_dir):
d = start_dir
for _ in range(20):
if not d:
break
pj = os.path.join(d, ".v8-project.json")
if os.path.isfile(pj):
return pj
parent = os.path.dirname(d)
if parent == d:
break
d = parent
return None
def same_path(a, b):
if not a or not b:
return False
try:
return os.path.abspath(a).rstrip("\\/").lower() == os.path.abspath(b).rstrip("\\/").lower()
except Exception:
return False
def find_project_database(args):
"""Запись базы в реестре, соответствующая переданному соединению. None, если не найдена."""
pf = _sg_find_v8project(os.getcwd())
if not pf:
return None
try:
with open(pf, encoding="utf-8-sig") as f:
proj = json.load(f)
except Exception:
return None
for db in proj.get("databases") or []:
if args.InfoBasePath and db.get("path") and same_path(db["path"], args.InfoBasePath):
return db
if args.InfoBaseServer and args.InfoBaseRef and db.get("server") and db.get("ref"):
if (db["server"].lower() == args.InfoBaseServer.lower()
and db["ref"].lower() == args.InfoBaseRef.lower()):
return db
return None
def _find_project_v8path(args):
"""Walk up from CWD to find .v8-project.json and read its v8path.
v8path записи базы сильнее корневого: в одном проекте базы живут на разных версиях
платформы, а версию формата выгрузки задаёт та платформа, которая выгружает.
"""
db = find_project_database(args)
if db and db.get("v8path"):
return db["v8path"]
d = os.getcwd() d = os.getcwd()
while True: while True:
pf = os.path.join(d, ".v8-project.json") pf = os.path.join(d, ".v8-project.json")
@@ -258,10 +312,10 @@ def _version_key(p):
return [int(x) for x in re.findall(r"\d+", _version_dir(p))] return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
def resolve_v8path(v8path): def resolve_v8path(v8path, args):
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly).""" """Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path: if not v8path:
v8path = _find_project_v8path() v8path = _find_project_v8path(args)
if not v8path: if not v8path:
if os.name == "nt": if os.name == "nt":
candidates = ( candidates = (
@@ -324,7 +378,7 @@ def main():
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath") args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
args.Execute = clean_path(args.Execute, "-Execute") args.Execute = clean_path(args.Execute, "-Execute")
v8path = resolve_v8path(args.V8Path) v8path = resolve_v8path(args.V8Path, args)
# --- Resolve additional arguments --- # --- Resolve additional arguments ---
# 1C:Enterprise is always launched by 1cv8 — ibcmd has no interactive mode. # 1C:Enterprise is always launched by 1cv8 — ibcmd has no interactive mode.
@@ -25,19 +25,19 @@ allowed-tools:
## Параметры подключения ## Параметры подключения
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: Прочитай `.v8-project.json` из корня проекта и разреши базу:
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую 1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` 2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default` 4. Если ветка не совпала — используй `default`
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files). Платформа — `v8path` найденной записи базы, иначе корневой `v8path`. Не задан ни там, ни там — `-V8Path` не передавай.
Если файла нет — предложи `/db-list add`. Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" <параметры> python ".roo/skills/db-update/scripts/db-update.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -75,11 +75,11 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" <п
```powershell ```powershell
# Обычное обновление (файловая база) # Обычное обновление (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" python ".roo/skills/db-update/scripts/db-update.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin"
# Динамическое обновление # Динамическое обновление
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -Dynamic on python ".roo/skills/db-update/scripts/db-update.py" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -Dynamic on
# Обновление расширения # Обновление расширения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Extension "МоёРасширение" python ".roo/skills/db-update/scripts/db-update.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Extension "МоёРасширение"
``` ```
@@ -1,4 +1,4 @@
# db-update v1.20 — Update 1C database configuration # db-update v1.21 — Update 1C database configuration
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
@@ -124,9 +124,10 @@ if ($Dynamic) { $Dynamic = if (@('on', 'yes', '+') -contains $Dynamic.ToLower())
$OutputEncoding = [System.Text.Encoding]::UTF8 $OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Реквизиты хранилища из .v8-project.json --- # --- Запись базы в .v8-project.json ---
# Модель их не передаёт: скрипт сопоставляет параметры соединения с записью в databases[] # Модель не передаёт ни путь к платформе конкретной базы, ни реквизиты хранилища: скрипт
# и берёт repository оттуда. Тот же приём, что в cf-edit.ps1 (сопоставление по configSrc). # сопоставляет параметры соединения с записью в databases[] и берёт их оттуда. Тот же приём,
# что в cf-edit.ps1 (сопоставление по configSrc).
function Find-V8Project([string]$startDir) { function Find-V8Project([string]$startDir) {
$d = $startDir $d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) { for ($i = 0; $i -lt 20 -and $d; $i++) {
@@ -410,6 +411,10 @@ Assert-InfoBaseExists $InfoBasePath
# --- Resolve V8Path --- # --- Resolve V8Path ---
function Find-ProjectV8Path { function Find-ProjectV8Path {
# v8path записи базы сильнее корневого: в одном проекте базы живут на разных версиях
# платформы, а версию формата выгрузки задаёт та платформа, которая выгружает.
$dbRec = Find-ProjectDatabase
if ($dbRec -and $dbRec.v8path) { return [string]$dbRec.v8path }
$dir = (Get-Location).Path $dir = (Get-Location).Path
while ($dir) { while ($dir) {
$pf = Join-Path $dir ".v8-project.json" $pf = Join-Path $dir ".v8-project.json"
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-update v1.20 — Update 1C database configuration # db-update v1.21 — Update 1C database configuration
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -37,8 +37,15 @@ def ci_parse_args(parser, argv=None):
def _find_project_v8path(): def _find_project_v8path(args):
"""Walk up from CWD to find .v8-project.json and read its v8path.""" """Walk up from CWD to find .v8-project.json and read its v8path.
v8path записи базы сильнее корневого: в одном проекте базы живут на разных версиях
платформы, а версию формата выгрузки задаёт та платформа, которая выгружает.
"""
db = find_project_database(args)
if db and db.get("v8path"):
return db["v8path"]
d = os.getcwd() d = os.getcwd()
while True: while True:
pf = os.path.join(d, ".v8-project.json") pf = os.path.join(d, ".v8-project.json")
@@ -87,9 +94,10 @@ V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP", "/ConfigurationRepositoryP"]
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"] IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
# --- Реквизиты хранилища из .v8-project.json --- # --- Запись базы в .v8-project.json ---
# Модель их не передаёт: скрипт сопоставляет параметры соединения с записью в databases[] # Модель не передаёт ни путь к платформе конкретной базы, ни реквизиты хранилища: скрипт
# и берёт repository оттуда. Тот же приём, что в cf-edit.py (сопоставление по configSrc). # сопоставляет параметры соединения с записью в databases[] и берёт их оттуда. Тот же приём,
# что в cf-edit.py (сопоставление по configSrc).
def _sg_find_v8project(start_dir): def _sg_find_v8project(start_dir):
d = start_dir d = start_dir
for _ in range(20): for _ in range(20):
@@ -328,10 +336,10 @@ def _version_key(p):
return [int(x) for x in re.findall(r"\d+", _version_dir(p))] return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
def resolve_v8path(v8path): def resolve_v8path(v8path, args):
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly).""" """Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path: if not v8path:
v8path = _find_project_v8path() v8path = _find_project_v8path(args)
if not v8path: if not v8path:
if os.name == "nt": if os.name == "nt":
candidates = ( candidates = (
@@ -659,7 +667,7 @@ def main():
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath") args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
assert_infobase_exists(args.InfoBasePath) assert_infobase_exists(args.InfoBasePath)
v8path = resolve_v8path(args.V8Path) v8path = resolve_v8path(args.V8Path, args)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8" engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
@@ -28,20 +28,20 @@ allowed-tools:
Предпочтительно использовать конкретную базу — это надёжнее. Временная база всё равно поднимается Предпочтительно использовать конкретную базу — это надёжнее. Временная база всё равно поднимается
под проверку исходников, если она не отключена. под проверку исходников, если она не отключена.
1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу: 1. Прочитай `.v8-project.json` из корня проекта и разреши базу:
2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую 2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` 3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
4. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 4. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
5. Если ветка не совпала — используй `default` 5. Если ветка не совпала — используй `default`
6. Если `.v8-project.json` нет или база не найдена — не указывай параметры подключения: скрипт автоматически создаст временную базу. Для EPF со ссылочными типами (CatalogRef, DocumentRef и т.д.) генерируются заглушки метаданных. Временная база удаляется после сборки. 6. Если `.v8-project.json` нет или база не найдена — не указывай параметры подключения: скрипт автоматически создаст временную базу. Для EPF со ссылочными типами (CatalogRef, DocumentRef и т.д.) генерируются заглушки метаданных. Временная база удаляется после сборки.
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files). Платформа — `v8path` найденной записи базы, иначе корневой `v8path`. Не задан ни там, ни там — `-V8Path` не передавай.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" <параметры> python ".roo/skills/epf-build/scripts/epf-build.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -76,8 +76,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" <п
```powershell ```powershell
# Сборка обработки (файловая база) # Сборка обработки (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf" python ".roo/skills/epf-build/scripts/epf-build.py" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf"
# Серверная база # Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf" python ".roo/skills/epf-build/scripts/epf-build.py" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf"
``` ```
@@ -1,4 +1,4 @@
# epf-build v1.17 — Build external data processor or report (EPF/ERF) from XML sources # epf-build v1.19 — Build external data processor or report (EPF/ERF) from XML sources
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
@@ -276,8 +276,52 @@ function Assert-InfoBaseExists {
Assert-InfoBaseExists $InfoBasePath Assert-InfoBaseExists $InfoBasePath
# --- Запись базы в .v8-project.json ---
# Модель не передаёт ни путь к платформе конкретной базы, ни реквизиты хранилища: скрипт
# сопоставляет параметры соединения с записью в databases[] и берёт их оттуда. Тот же приём,
# что в cf-edit.ps1 (сопоставление по configSrc).
function Find-V8Project([string]$startDir) {
$d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) {
$pj = Join-Path $d ".v8-project.json"
if (Test-Path $pj) { return $pj }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
return $null
}
function Test-SamePath {
param([string]$A, [string]$B)
if (-not $A -or -not $B) { return $false }
try {
$na = [System.IO.Path]::GetFullPath($A).TrimEnd('\', '/')
$nb = [System.IO.Path]::GetFullPath($B).TrimEnd('\', '/')
return $na.Equals($nb, [System.StringComparison]::OrdinalIgnoreCase)
} catch { return $false }
}
function Find-ProjectDatabase {
# Запись базы в реестре, соответствующая переданному соединению. $null, если не найдена.
$pf = Find-V8Project (Get-Location).Path
if (-not $pf) { return $null }
try { $proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json } catch { return $null }
if (-not $proj.databases) { return $null }
foreach ($db in $proj.databases) {
if ($InfoBasePath -and $db.path -and (Test-SamePath $db.path $InfoBasePath)) { return $db }
if ($InfoBaseServer -and $InfoBaseRef -and $db.server -and $db.ref) {
if ($db.server.Equals($InfoBaseServer, [System.StringComparison]::OrdinalIgnoreCase) -and
$db.ref.Equals($InfoBaseRef, [System.StringComparison]::OrdinalIgnoreCase)) { return $db }
}
}
return $null
}
# --- Resolve V8Path --- # --- Resolve V8Path ---
function Find-ProjectV8Path { function Find-ProjectV8Path {
# v8path записи базы сильнее корневого: в одном проекте базы живут на разных версиях
# платформы, а версию формата выгрузки задаёт та платформа, которая выгружает.
$dbRec = Find-ProjectDatabase
if ($dbRec -and $dbRec.v8path) { return [string]$dbRec.v8path }
$dir = (Get-Location).Path $dir = (Get-Location).Path
while ($dir) { while ($dir) {
$pf = Join-Path $dir ".v8-project.json" $pf = Join-Path $dir ".v8-project.json"
@@ -397,18 +441,6 @@ function Test-OutputNonEmpty {
return (Test-Path $Path -PathType Leaf) -and ((Get-Item $Path -ErrorAction SilentlyContinue).Length -gt 0) return (Test-Path $Path -PathType Leaf) -and ((Get-Item $Path -ErrorAction SilentlyContinue).Length -gt 0)
} }
function Find-V8Project([string]$startDir) {
$d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) {
$pj = Join-Path $d ".v8-project.json"
if (Test-Path $pj) { return $pj }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
return $null
}
# --- Проверка исходников платформой --- # --- Проверка исходников платформой ---
# Сборка .epf/.erf ничего не проверяет: /LoadExternalDataProcessorOrReportFromFiles упаковывает XML # Сборка .epf/.erf ничего не проверяет: /LoadExternalDataProcessorOrReportFromFiles упаковывает XML
# и модули не компилирует, поэтому сломанный модуль доезжает до пользователя и падает при открытии # и модули не компилирует, поэтому сломанный модуль доезжает до пользователя и падает при открытии
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# epf-build v1.17 — Build external data processor or report (EPF/ERF) from XML sources # epf-build v1.19 — Build external data processor or report (EPF/ERF) from XML sources
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -38,8 +38,48 @@ def ci_parse_args(parser, argv=None):
def _find_project_v8path(): # --- Запись базы в .v8-project.json ---
"""Walk up from CWD to find .v8-project.json and read its v8path.""" # Модель не передаёт ни путь к платформе конкретной базы, ни реквизиты хранилища: скрипт
# сопоставляет параметры соединения с записью в databases[] и берёт их оттуда. Тот же приём,
# что в cf-edit.py (сопоставление по configSrc).
def same_path(a, b):
if not a or not b:
return False
try:
return os.path.abspath(a).rstrip("\\/").lower() == os.path.abspath(b).rstrip("\\/").lower()
except Exception:
return False
def find_project_database(args):
"""Запись базы в реестре, соответствующая переданному соединению. None, если не найдена."""
pf = _sg_find_v8project(os.getcwd())
if not pf:
return None
try:
with open(pf, encoding="utf-8-sig") as f:
proj = json.load(f)
except Exception:
return None
for db in proj.get("databases") or []:
if args.InfoBasePath and db.get("path") and same_path(db["path"], args.InfoBasePath):
return db
if args.InfoBaseServer and args.InfoBaseRef and db.get("server") and db.get("ref"):
if (db["server"].lower() == args.InfoBaseServer.lower()
and db["ref"].lower() == args.InfoBaseRef.lower()):
return db
return None
def _find_project_v8path(args):
"""Walk up from CWD to find .v8-project.json and read its v8path.
v8path записи базы сильнее корневого: в одном проекте базы живут на разных версиях
платформы, а версию формата выгрузки задаёт та платформа, которая выгружает.
"""
db = find_project_database(args)
if db and db.get("v8path"):
return db["v8path"]
d = os.getcwd() d = os.getcwd()
while True: while True:
pf = os.path.join(d, ".v8-project.json") pf = os.path.join(d, ".v8-project.json")
@@ -244,10 +284,10 @@ def _version_key(p):
return [int(x) for x in re.findall(r"\d+", _version_dir(p))] return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
def resolve_v8path(v8path): def resolve_v8path(v8path, args):
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly).""" """Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path: if not v8path:
v8path = _find_project_v8path() v8path = _find_project_v8path(args)
if not v8path: if not v8path:
if os.name == "nt": if os.name == "nt":
candidates = ( candidates = (
@@ -576,7 +616,7 @@ def main():
args.OutputFile = clean_path(args.OutputFile, "-OutputFile") args.OutputFile = clean_path(args.OutputFile, "-OutputFile")
# --- Resolve V8Path --- # --- Resolve V8Path ---
v8path = resolve_v8path(args.V8Path) v8path = resolve_v8path(args.V8Path, args)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8" engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Resolve additional arguments for the selected engine --- # --- Resolve additional arguments for the selected engine ---
@@ -1,4 +1,4 @@
# stub-db-create v1.10 — Create temp 1C infobase with metadata stubs for EPF/ERF build # stub-db-create v1.11 — Create temp 1C infobase with metadata stubs for EPF/ERF build
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -387,6 +387,16 @@ if ($needsRegistrator) {
# #
# Подстановка типа делается ТОЛЬКО в .xml (это DefaultForm и основной реквизит формы); в .bsl # Подстановка типа делается ТОЛЬКО в .xml (это DefaultForm и основной реквизит формы); в .bsl
# такой же текст был бы кодом, и трогать его нельзя. # такой же текст был бы кодом, и трогать его нельзя.
function Convert-RefTags {
param([string]$Text, [string]$ExtTag, [string]$CfgTag)
# Только префикс вида объекта в начале квалифицированного имени. Глобальная замена подстроки
# резала бы и имя самого объекта, если оно оканчивается так же (ПриёмкаExternalReport), —
# ссылка расходилась с именем, и заглушка не грузилась. [regex]::Replace, а не -replace:
# оператор регистронезависим, а теги 1С регистрозависимы.
return [regex]::Replace($Text, "(?<![\w.])$ExtTag(Object)?\.", ($CfgTag + '$1.'))
}
function Add-SourceObjectToConfig { function Add-SourceObjectToConfig {
param([string]$SourceXml, [string]$CfgDir) param([string]$SourceXml, [string]$CfgDir)
@@ -416,7 +426,7 @@ function Add-SourceObjectToConfig {
$conv = $reGuid.Replace($text, $reissue) $conv = $reGuid.Replace($text, $reissue)
$conv = $conv.Replace("<$extTag ", "<$cfgTag ").Replace("<$extTag>", "<$cfgTag>").Replace("</$extTag>", "</$cfgTag>") $conv = $conv.Replace("<$extTag ", "<$cfgTag ").Replace("<$extTag>", "<$cfgTag>").Replace("</$extTag>", "</$cfgTag>")
$conv = $conv.Replace("${extTag}Object.", "${cfgTag}Object.").Replace("$extTag.", "$cfgTag.") $conv = Convert-RefTags $conv $extTag $cfgTag
# Тип менеджера у внешней обработки не объявлен, а объекту конфигурации он обязателен: # Тип менеджера у внешней обработки не объявлен, а объекту конфигурации он обязателен:
# без него платформа отвечает «отсутствует один или более типов объекта». # без него платформа отвечает «отсутствует один или более типов объекта».
@@ -454,7 +464,7 @@ function Add-SourceObjectToConfig {
if ($f.Extension -ieq '.xml') { if ($f.Extension -ieq '.xml') {
$t = [IO.File]::ReadAllText($f.FullName, [Text.Encoding]::UTF8) $t = [IO.File]::ReadAllText($f.FullName, [Text.Encoding]::UTF8)
$t = $reGuid.Replace($t, $reissue) $t = $reGuid.Replace($t, $reissue)
$t = $t.Replace("${extTag}Object.", "${cfgTag}Object.").Replace("$extTag.", "$cfgTag.") $t = Convert-RefTags $t $extTag $cfgTag
[IO.File]::WriteAllText($dst, $t, $encBom) [IO.File]::WriteAllText($dst, $t, $encBom)
} else { } else {
Copy-Item -Path $f.FullName -Destination $dst -Force Copy-Item -Path $f.FullName -Destination $dst -Force
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# stub-db-create v1.10 — Create temp 1C infobase with metadata stubs for EPF/ERF build # stub-db-create v1.11 — Create temp 1C infobase with metadata stubs for EPF/ERF build
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -1133,6 +1133,14 @@ def write_bom(path, content):
GUID_RE = re.compile(r'[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}') GUID_RE = re.compile(r'[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}')
def retag_refs(text, ext_tag, cfg_tag):
# Только префикс вида объекта в начале квалифицированного имени. Глобальная замена подстроки
# резала бы и имя самого объекта, если оно оканчивается так же (ПриёмкаExternalReport), —
# ссылка расходилась с именем, и заглушка не грузилась.
return re.sub(r'(?<![\w.])%s(Object)?\.' % ext_tag,
lambda m: cfg_tag + (m.group(1) or '') + '.', text)
def add_source_object_to_config(source_xml, cfg_dir): def add_source_object_to_config(source_xml, cfg_dir):
# Копия объекта живёт в конфигурации базы, а следом в ту же базу грузится исходник как ВНЕШНЯЯ # Копия объекта живёт в конфигурации базы, а следом в ту же базу грузится исходник как ВНЕШНЯЯ
# обработка. С одинаковыми идентификаторами платформа путает их и через раз отвечает «Исключение # обработка. С одинаковыми идентификаторами платформа путает их и через раз отвечает «Исключение
@@ -1163,7 +1171,7 @@ def add_source_object_to_config(source_xml, cfg_dir):
conv = reissue(text) conv = reissue(text)
conv = conv.replace('<%s ' % ext_tag, '<%s ' % cfg_tag).replace('<%s>' % ext_tag, '<%s>' % cfg_tag) conv = conv.replace('<%s ' % ext_tag, '<%s ' % cfg_tag).replace('<%s>' % ext_tag, '<%s>' % cfg_tag)
conv = conv.replace('</%s>' % ext_tag, '</%s>' % cfg_tag) conv = conv.replace('</%s>' % ext_tag, '</%s>' % cfg_tag)
conv = conv.replace('%sObject.' % ext_tag, '%sObject.' % cfg_tag).replace('%s.' % ext_tag, '%s.' % cfg_tag) conv = retag_refs(conv, ext_tag, cfg_tag)
# Тип менеджера у внешней обработки не объявлен, а объекту конфигурации он обязателен: # Тип менеджера у внешней обработки не объявлен, а объекту конфигурации он обязателен:
# без него платформа отвечает «отсутствует один или более типов объекта». # без него платформа отвечает «отсутствует один или более типов объекта».
@@ -1200,7 +1208,7 @@ def add_source_object_to_config(source_xml, cfg_dir):
with io.open(full, encoding='utf-8-sig') as fh: with io.open(full, encoding='utf-8-sig') as fh:
t = fh.read() t = fh.read()
t = reissue(t) t = reissue(t)
t = t.replace('%sObject.' % ext_tag, '%sObject.' % cfg_tag).replace('%s.' % ext_tag, '%s.' % cfg_tag) t = retag_refs(t, ext_tag, cfg_tag)
write_bom(dst, t) write_bom(dst, t)
else: else:
shutil.copyfile(full, dst) shutil.copyfile(full, dst)
@@ -26,20 +26,20 @@ allowed-tools:
Для разборки EPF/ERF требуется информационная база с конфигурацией. Без базы ссылочные типы безвозвратно теряются. Для разборки EPF/ERF требуется информационная база с конфигурацией. Без базы ссылочные типы безвозвратно теряются.
1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу: 1. Прочитай `.v8-project.json` из корня проекта и разреши базу:
2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую 2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` 3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
4. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 4. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
5. Если ветка не совпала — используй `default` 5. Если ветка не совпала — используй `default`
6. Если `.v8-project.json` нет или база не найдена — **сообщи пользователю об ошибке**. Для dump база обязательна: в пустой базе ссылочные типы (CatalogRef, DocumentRef и т.д.) безвозвратно сбрасываются в строки. Предложи указать базу или зарегистрировать через `/db-list add`. 6. Если `.v8-project.json` нет или база не найдена — **сообщи пользователю об ошибке**. Для dump база обязательна: в пустой базе ссылочные типы (CatalogRef, DocumentRef и т.д.) безвозвратно сбрасываются в строки. Предложи указать базу или зарегистрировать через `/db-list add`.
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files). Платформа — `v8path` найденной записи базы, иначе корневой `v8path`. Не задан ни там, ни там — `-V8Path` не передавай.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" <параметры> python ".roo/skills/epf-dump/scripts/epf-dump.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -64,8 +64,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" <па
```powershell ```powershell
# Разборка обработки (файловая база) # Разборка обработки (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МояОбработка.epf" -OutputDir "src" python ".roo/skills/epf-dump/scripts/epf-dump.py" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МояОбработка.epf" -OutputDir "src"
# Серверная база # Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МояОбработка.epf" -OutputDir "src" python ".roo/skills/epf-dump/scripts/epf-dump.py" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МояОбработка.epf" -OutputDir "src"
``` ```
@@ -1,4 +1,4 @@
# epf-dump v1.15 — Dump external data processor or report (EPF/ERF) to XML sources # epf-dump v1.16 — Dump external data processor or report (EPF/ERF) to XML sources
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
@@ -267,8 +267,52 @@ function Assert-InfoBaseExists {
Assert-InfoBaseExists $InfoBasePath Assert-InfoBaseExists $InfoBasePath
# --- Запись базы в .v8-project.json ---
# Модель не передаёт ни путь к платформе конкретной базы, ни реквизиты хранилища: скрипт
# сопоставляет параметры соединения с записью в databases[] и берёт их оттуда. Тот же приём,
# что в cf-edit.ps1 (сопоставление по configSrc).
function Find-V8Project([string]$startDir) {
$d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) {
$pj = Join-Path $d ".v8-project.json"
if (Test-Path $pj) { return $pj }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
return $null
}
function Test-SamePath {
param([string]$A, [string]$B)
if (-not $A -or -not $B) { return $false }
try {
$na = [System.IO.Path]::GetFullPath($A).TrimEnd('\', '/')
$nb = [System.IO.Path]::GetFullPath($B).TrimEnd('\', '/')
return $na.Equals($nb, [System.StringComparison]::OrdinalIgnoreCase)
} catch { return $false }
}
function Find-ProjectDatabase {
# Запись базы в реестре, соответствующая переданному соединению. $null, если не найдена.
$pf = Find-V8Project (Get-Location).Path
if (-not $pf) { return $null }
try { $proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json } catch { return $null }
if (-not $proj.databases) { return $null }
foreach ($db in $proj.databases) {
if ($InfoBasePath -and $db.path -and (Test-SamePath $db.path $InfoBasePath)) { return $db }
if ($InfoBaseServer -and $InfoBaseRef -and $db.server -and $db.ref) {
if ($db.server.Equals($InfoBaseServer, [System.StringComparison]::OrdinalIgnoreCase) -and
$db.ref.Equals($InfoBaseRef, [System.StringComparison]::OrdinalIgnoreCase)) { return $db }
}
}
return $null
}
# --- Resolve V8Path --- # --- Resolve V8Path ---
function Find-ProjectV8Path { function Find-ProjectV8Path {
# v8path записи базы сильнее корневого: в одном проекте базы живут на разных версиях
# платформы, а версию формата выгрузки задаёт та платформа, которая выгружает.
$dbRec = Find-ProjectDatabase
if ($dbRec -and $dbRec.v8path) { return [string]$dbRec.v8path }
$dir = (Get-Location).Path $dir = (Get-Location).Path
while ($dir) { while ($dir) {
$pf = Join-Path $dir ".v8-project.json" $pf = Join-Path $dir ".v8-project.json"
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# epf-dump v1.15 — Dump external data processor or report (EPF/ERF) to XML sources # epf-dump v1.16 — Dump external data processor or report (EPF/ERF) to XML sources
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -37,8 +37,62 @@ def ci_parse_args(parser, argv=None):
def _find_project_v8path(): # --- Запись базы в .v8-project.json ---
"""Walk up from CWD to find .v8-project.json and read its v8path.""" # Модель не передаёт ни путь к платформе конкретной базы, ни реквизиты хранилища: скрипт
# сопоставляет параметры соединения с записью в databases[] и берёт их оттуда. Тот же приём,
# что в cf-edit.py (сопоставление по configSrc).
def _sg_find_v8project(start_dir):
d = start_dir
for _ in range(20):
if not d:
break
pj = os.path.join(d, ".v8-project.json")
if os.path.isfile(pj):
return pj
parent = os.path.dirname(d)
if parent == d:
break
d = parent
return None
def same_path(a, b):
if not a or not b:
return False
try:
return os.path.abspath(a).rstrip("\\/").lower() == os.path.abspath(b).rstrip("\\/").lower()
except Exception:
return False
def find_project_database(args):
"""Запись базы в реестре, соответствующая переданному соединению. None, если не найдена."""
pf = _sg_find_v8project(os.getcwd())
if not pf:
return None
try:
with open(pf, encoding="utf-8-sig") as f:
proj = json.load(f)
except Exception:
return None
for db in proj.get("databases") or []:
if args.InfoBasePath and db.get("path") and same_path(db["path"], args.InfoBasePath):
return db
if args.InfoBaseServer and args.InfoBaseRef and db.get("server") and db.get("ref"):
if (db["server"].lower() == args.InfoBaseServer.lower()
and db["ref"].lower() == args.InfoBaseRef.lower()):
return db
return None
def _find_project_v8path(args):
"""Walk up from CWD to find .v8-project.json and read its v8path.
v8path записи базы сильнее корневого: в одном проекте базы живут на разных версиях
платформы, а версию формата выгрузки задаёт та платформа, которая выгружает.
"""
db = find_project_database(args)
if db and db.get("v8path"):
return db["v8path"]
d = os.getcwd() d = os.getcwd()
while True: while True:
pf = os.path.join(d, ".v8-project.json") pf = os.path.join(d, ".v8-project.json")
@@ -243,10 +297,10 @@ def _version_key(p):
return [int(x) for x in re.findall(r"\d+", _version_dir(p))] return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
def resolve_v8path(v8path): def resolve_v8path(v8path, args):
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly).""" """Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path: if not v8path:
v8path = _find_project_v8path() v8path = _find_project_v8path(args)
if not v8path: if not v8path:
if os.name == "nt": if os.name == "nt":
candidates = ( candidates = (
@@ -446,7 +500,7 @@ def main():
args.OutputDir = clean_path(args.OutputDir, "-OutputDir") args.OutputDir = clean_path(args.OutputDir, "-OutputDir")
# --- Resolve V8Path --- # --- Resolve V8Path ---
v8path = resolve_v8path(args.V8Path) v8path = resolve_v8path(args.V8Path, args)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8" engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Resolve additional arguments for the selected engine --- # --- Resolve additional arguments for the selected engine ---
@@ -37,7 +37,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-FormatVersion "<версия>"] python ".roo/skills/epf-init/scripts/init.py" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-FormatVersion "<версия>"]
``` ```
## Дальнейшие шаги ## Дальнейшие шаги
@@ -26,7 +26,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-validate.ps1" -ObjectPath "src/МояОбработка" python ".roo/skills/epf-validate/scripts/epf-validate.py" -ObjectPath "src/МояОбработка"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-validate.ps1" -ObjectPath "src/МояОбработка/МояОбработка.xml" python ".roo/skills/epf-validate/scripts/epf-validate.py" -ObjectPath "src/МояОбработка/МояОбработка.xml"
``` ```
@@ -28,14 +28,14 @@ allowed-tools:
Предпочтительно использовать конкретную базу — это надёжнее. Временная база всё равно поднимается Предпочтительно использовать конкретную базу — это надёжнее. Временная база всё равно поднимается
под проверку исходников, если она не отключена. под проверку исходников, если она не отключена.
1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу: 1. Прочитай `.v8-project.json` из корня проекта и разреши базу:
2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую 2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` 3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
4. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 4. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
5. Если ветка не совпала — используй `default` 5. Если ветка не совпала — используй `default`
6. Если `.v8-project.json` нет или база не найдена — не указывай параметры подключения: скрипт автоматически создаст временную базу. Для ERF со ссылочными типами (CatalogRef, DocumentRef и т.д.) генерируются заглушки метаданных. Временная база удаляется после сборки. 6. Если `.v8-project.json` нет или база не найдена — не указывай параметры подключения: скрипт автоматически создаст временную базу. Для ERF со ссылочными типами (CatalogRef, DocumentRef и т.д.) генерируются заглушки метаданных. Временная база удаляется после сборки.
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files). Платформа — `v8path` найденной записи базы, иначе корневой `v8path`. Не задан ни там, ни там — `-V8Path` не передавай.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда ## Команда
@@ -43,7 +43,7 @@ allowed-tools:
Используй общий скрипт из epf-build: Используй общий скрипт из epf-build:
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.ps1" <параметры> python ".roo/skills/epf-build/scripts/epf-build.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -78,8 +78,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-bu
```powershell ```powershell
# Сборка отчёта (файловая база) # Сборка отчёта (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.ps1" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf" python ".roo/skills/epf-build/scripts/epf-build.py" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf"
# Серверная база # Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf" python ".roo/skills/epf-build/scripts/epf-build.py" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf"
``` ```
@@ -26,14 +26,14 @@ allowed-tools:
Для разборки EPF/ERF требуется информационная база с конфигурацией. Без базы ссылочные типы безвозвратно теряются. Для разборки EPF/ERF требуется информационная база с конфигурацией. Без базы ссылочные типы безвозвратно теряются.
1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу: 1. Прочитай `.v8-project.json` из корня проекта и разреши базу:
2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую 2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` 3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
4. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 4. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
5. Если ветка не совпала — используй `default` 5. Если ветка не совпала — используй `default`
6. Если `.v8-project.json` нет или база не найдена — **сообщи пользователю об ошибке**. Для dump база обязательна: в пустой базе ссылочные типы (CatalogRef, DocumentRef и т.д.) безвозвратно сбрасываются в строки. Предложи указать базу или зарегистрировать через `/db-list add`. 6. Если `.v8-project.json` нет или база не найдена — **сообщи пользователю об ошибке**. Для dump база обязательна: в пустой базе ссылочные типы (CatalogRef, DocumentRef и т.д.) безвозвратно сбрасываются в строки. Предложи указать базу или зарегистрировать через `/db-list add`.
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files). Платформа — `v8path` найденной записи базы, иначе корневой `v8path`. Не задан ни там, ни там — `-V8Path` не передавай.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда ## Команда
@@ -41,7 +41,7 @@ allowed-tools:
Используй общий скрипт из epf-dump: Используй общий скрипт из epf-dump:
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.ps1" <параметры> python ".roo/skills/epf-dump/scripts/epf-dump.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -66,8 +66,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dum
```powershell ```powershell
# Разборка отчёта (файловая база) # Разборка отчёта (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.ps1" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МойОтчёт.erf" -OutputDir "src" python ".roo/skills/epf-dump/scripts/epf-dump.py" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МойОтчёт.erf" -OutputDir "src"
# Серверная база # Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МойОтчёт.erf" -OutputDir "src" python ".roo/skills/epf-dump/scripts/epf-dump.py" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МойОтчёт.erf" -OutputDir "src"
``` ```
@@ -38,7 +38,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-FormatVersion "<версия>"] [-WithSKD] python ".roo/skills/erf-init/scripts/init.py" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-FormatVersion "<версия>"] [-WithSKD]
``` ```
## Дальнейшие шаги ## Дальнейшие шаги

Some files were not shown because too many files have changed in this diff Show More