diff --git a/.claude/skills/cf-edit/SKILL.md b/.claude/skills/cf-edit/SKILL.md new file mode 100644 index 00000000..0269df3d --- /dev/null +++ b/.claude/skills/cf-edit/SKILL.md @@ -0,0 +1,58 @@ +--- +name: cf-edit +description: Точечное редактирование конфигурации 1С. Используй когда нужно изменить свойства конфигурации, добавить или удалить объект из состава, настроить роли по умолчанию +argument-hint: -ConfigPath -Operation -Value +allowed-tools: + - Bash + - Read + - Write + - Glob +--- + +# /cf-edit — редактирование конфигурации 1С + +Точечное редактирование Configuration.xml: свойства, состав ChildObjects, роли по умолчанию. + +## Параметры и команда + +| Параметр | Описание | +|----------|----------| +| `ConfigPath` | Путь к Configuration.xml или каталогу выгрузки | +| `Operation` | Операция (см. таблицу) | +| `Value` | Значение для операции (batch через `;;`) | +| `DefinitionFile` | JSON-файл с массивом операций | +| `NoValidate` | Пропустить авто-валидацию | + +```powershell +powershell.exe -NoProfile -File .claude\skills\cf-edit\scripts\cf-edit.ps1 -ConfigPath '' -Operation modify-property -Value 'Version=1.0.0.1' +``` + +## Операции + +| Операция | Формат Value | Описание | +|----------|-------------|----------| +| `modify-property` | `Ключ=Значение` (batch `;;`) | Изменить свойство | +| `add-childObject` | `Type.Name` (batch `;;`) | Добавить объект в ChildObjects | +| `remove-childObject` | `Type.Name` (batch `;;`) | Удалить объект из ChildObjects | +| `add-defaultRole` | `Role.Name` или `Name` | Добавить роль по умолчанию | +| `remove-defaultRole` | `Role.Name` или `Name` | Удалить роль по умолчанию | +| `set-defaultRoles` | Имена через `;;` | Заменить список ролей по умолчанию | + +Подробнее: `reference.md` в каталоге навыка. + +## Примеры + +```powershell +# Изменить версию и поставщика +... -ConfigPath test-tmp/cf -Operation modify-property -Value "Version=1.0.0.1 ;; Vendor=Фирма 1С" + +# Добавить объекты +... -ConfigPath test-tmp/cf -Operation add-childObject -Value "Catalog.Товары ;; Document.Заказ" + +# Удалить объект +... -ConfigPath test-tmp/cf -Operation remove-childObject -Value "Catalog.Устаревший" + +# Роли по умолчанию +... -ConfigPath test-tmp/cf -Operation add-defaultRole -Value "ПолныеПрава" +... -ConfigPath test-tmp/cf -Operation set-defaultRoles -Value "ПолныеПрава ;; Администратор" +``` diff --git a/.claude/skills/cf-edit/reference.md b/.claude/skills/cf-edit/reference.md new file mode 100644 index 00000000..02c6a36a --- /dev/null +++ b/.claude/skills/cf-edit/reference.md @@ -0,0 +1,63 @@ +# cf-edit — справочник операций + +## modify-property + +Свойства для редактирования: + +### Скалярные +`Name`, `Version`, `Vendor`, `Comment`, `NamePrefix`, `UpdateCatalogAddress` + +### LocalString (многоязычные) +`Synonym`, `BriefInformation`, `DetailedInformation`, `Copyright`, `VendorInformationAddress`, `ConfigurationInformationAddress` + +### Enum +| Свойство | Допустимые значения | +|----------|---------------------| +| `CompatibilityMode` | `Version8_3_20` ... `Version8_3_27`, `DontUse` | +| `ConfigurationExtensionCompatibilityMode` | то же | +| `DefaultRunMode` | `ManagedApplication`, `OrdinaryApplication`, `Auto` | +| `ScriptVariant` | `Russian`, `English` | +| `DataLockControlMode` | `Managed`, `Automatic`, `AutomaticAndManaged` | +| `ObjectAutonumerationMode` | `NotAutoFree`, `AutoFree` | +| `ModalityUseMode` | `DontUse`, `Use`, `UseWithWarnings` | +| `SynchronousPlatformExtensionAndAddInCallUseMode` | `DontUse`, `Use`, `UseWithWarnings` | +| `InterfaceCompatibilityMode` | `Taxi`, `TaxiEnableVersion8_2`, `Version8_2` | +| `DatabaseTablespacesUseMode` | `DontUse`, `Use` | +| `MainClientApplicationWindowMode` | `Normal`, `Fullscreen`, `Kiosk` | + +### Ref +`DefaultLanguage` — значение вида `Language.Русский` + +### Формат batch +`"Version=1.0.0.1 ;; Vendor=Фирма 1С ;; Synonym=Тестовая конфигурация"` + +## add-childObject / remove-childObject + +Формат: `Type.Name` — XML-тип и имя объекта через точку. + +При добавлении объект вставляется в каноническую позицию: +1. Находит последний элемент того же типа → вставляет после +2. Если тип отсутствует → находит последний элемент предшествующего типа → вставляет после +3. Внутри одного типа — алфавитный порядок + +Batch: `"Catalog.Товары ;; Document.Заказ ;; Enum.ВидыОплат"` + +## add-defaultRole / remove-defaultRole / set-defaultRoles + +Имя роли: `ПолныеПрава` или `Role.ПолныеПрава` (префикс `Role.` добавляется автоматически). + +`set-defaultRoles` полностью заменяет список ролей. + +## DefinitionFile (JSON) + +```json +[ + { "operation": "modify-property", "value": "Version=2.0.0.1 ;; Vendor=Test" }, + { "operation": "add-childObject", "value": "Catalog.Товары ;; Document.Заказ" }, + { "operation": "add-defaultRole", "value": "ПолныеПрава" } +] +``` + +## Авто-валидация + +После сохранения автоматически запускается `cf-validate` (если не указан `-NoValidate`). diff --git a/.claude/skills/cf-edit/scripts/cf-edit.ps1 b/.claude/skills/cf-edit/scripts/cf-edit.ps1 new file mode 100644 index 00000000..fcf91b47 --- /dev/null +++ b/.claude/skills/cf-edit/scripts/cf-edit.ps1 @@ -0,0 +1,523 @@ +# cf-edit v1.0 — Edit 1C configuration root (Configuration.xml) +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)][string]$ConfigPath, + [string]$DefinitionFile, + [ValidateSet("modify-property","add-childObject","remove-childObject","add-defaultRole","remove-defaultRole","set-defaultRoles")] + [string]$Operation, + [string]$Value, + [switch]$NoValidate +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Mode validation --- +if ($DefinitionFile -and $Operation) { Write-Error "Cannot use both -DefinitionFile and -Operation"; exit 1 } +if (-not $DefinitionFile -and -not $Operation) { Write-Error "Either -DefinitionFile or -Operation is required"; exit 1 } + +# --- Resolve path --- +if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) { + $ConfigPath = Join-Path (Get-Location).Path $ConfigPath +} +if (Test-Path $ConfigPath -PathType Container) { + $candidate = Join-Path $ConfigPath "Configuration.xml" + if (Test-Path $candidate) { $ConfigPath = $candidate } + else { Write-Error "No Configuration.xml in directory"; exit 1 } +} +if (-not (Test-Path $ConfigPath)) { Write-Error "File not found: $ConfigPath"; exit 1 } +$resolvedPath = (Resolve-Path $ConfigPath).Path + +# --- Load XML with PreserveWhitespace --- +$script:xmlDoc = New-Object System.Xml.XmlDocument +$script:xmlDoc.PreserveWhitespace = $true +$script:xmlDoc.Load($resolvedPath) + +$script:addCount = 0 +$script:removeCount = 0 +$script:modifyCount = 0 + +function Info([string]$msg) { Write-Host "[INFO] $msg" } +function Warn([string]$msg) { Write-Host "[WARN] $msg" } + +# --- Detect structure --- +$root = $script:xmlDoc.DocumentElement +$script:mdNs = "http://v8.1c.ru/8.3/MDClasses" +$script:xrNs = "http://v8.1c.ru/8.3/xcf/readable" +$script:xsiNs = "http://www.w3.org/2001/XMLSchema-instance" +$script:v8Ns = "http://v8.1c.ru/8.1/data/core" + +$script:cfgEl = $null +foreach ($child in $root.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Configuration") { + $script:cfgEl = $child; break + } +} +if (-not $script:cfgEl) { Write-Error "No element found"; exit 1 } + +$script:propsEl = $null +$script:childObjsEl = $null +foreach ($child in $script:cfgEl.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + if ($child.LocalName -eq "Properties") { $script:propsEl = $child } + if ($child.LocalName -eq "ChildObjects") { $script:childObjsEl = $child } +} + +$script:objName = "" +foreach ($child in $script:propsEl.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Name") { + $script:objName = $child.InnerText.Trim(); break + } +} +Info "Configuration: $($script:objName)" + +# --- Canonical type order for ChildObjects (44 types) --- +$script:typeOrder = @( + "Language","Subsystem","StyleItem","Style", + "CommonPicture","SessionParameter","Role","CommonTemplate", + "FilterCriterion","CommonModule","CommonAttribute","ExchangePlan", + "XDTOPackage","WebService","HTTPService","WSReference", + "EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption", + "FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup", + "Constant","CommonForm","Catalog","Document", + "DocumentNumerator","Sequence","DocumentJournal","Enum", + "Report","DataProcessor","InformationRegister","AccumulationRegister", + "ChartOfCharacteristicTypes","ChartOfAccounts","AccountingRegister", + "ChartOfCalculationTypes","CalculationRegister", + "BusinessProcess","Task","IntegrationService" +) + +# --- XML manipulation helpers (from subsystem-edit pattern) --- +function Get-ChildIndent($container) { + foreach ($child in $container.ChildNodes) { + if ($child.NodeType -eq 'Whitespace' -or $child.NodeType -eq 'SignificantWhitespace') { + if ($child.Value -match '^\r?\n(\t+)$') { return $Matches[1] } + if ($child.Value -match '^\r?\n(\t+)') { return $Matches[1] } + } + } + $depth = 0; $current = $container + while ($current -and $current -ne $script:xmlDoc.DocumentElement) { $depth++; $current = $current.ParentNode } + return "`t" * ($depth + 1) +} + +function Insert-BeforeElement($container, $newNode, $refNode, $childIndent) { + $ws = $script:xmlDoc.CreateWhitespace("`r`n$childIndent") + if ($refNode) { + $container.InsertBefore($ws, $refNode) | Out-Null + $container.InsertBefore($newNode, $ws) | Out-Null + } else { + $trailing = $container.LastChild + if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) { + $container.InsertBefore($ws, $trailing) | Out-Null + $container.InsertBefore($newNode, $trailing) | Out-Null + } else { + $container.AppendChild($ws) | Out-Null + $container.AppendChild($newNode) | Out-Null + $parentIndent = if ($childIndent.Length -gt 1) { $childIndent.Substring(0, $childIndent.Length - 1) } else { "" } + $closeWs = $script:xmlDoc.CreateWhitespace("`r`n$parentIndent") + $container.AppendChild($closeWs) | Out-Null + } + } +} + +function Remove-NodeWithWhitespace($node) { + $parent = $node.ParentNode + $prev = $node.PreviousSibling + $next = $node.NextSibling + if ($prev -and ($prev.NodeType -eq 'Whitespace' -or $prev.NodeType -eq 'SignificantWhitespace')) { + $parent.RemoveChild($prev) | Out-Null + } elseif ($next -and ($next.NodeType -eq 'Whitespace' -or $next.NodeType -eq 'SignificantWhitespace')) { + $parent.RemoveChild($next) | Out-Null + } + $parent.RemoveChild($node) | Out-Null +} + +function Expand-SelfClosingElement($container, $parentIndent) { + if (-not $container.HasChildNodes -or $container.IsEmpty) { + $closeWs = $script:xmlDoc.CreateWhitespace("`r`n$parentIndent") + $container.AppendChild($closeWs) | Out-Null + } +} + +function Import-Fragment([string]$xmlString) { + $wrapper = "<_W xmlns=`"$($script:mdNs)`" xmlns:xsi=`"$($script:xsiNs)`" xmlns:v8=`"$($script:v8Ns)`" xmlns:xr=`"$($script:xrNs)`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`">$xmlString" + $frag = New-Object System.Xml.XmlDocument + $frag.PreserveWhitespace = $true + $frag.LoadXml($wrapper) + $nodes = @() + foreach ($child in $frag.DocumentElement.ChildNodes) { + if ($child.NodeType -eq 'Element') { + $nodes += $script:xmlDoc.ImportNode($child, $true) + } + } + return ,$nodes +} + +# --- Parse batch value (split by ;;) --- +function Parse-BatchValue([string]$val) { + $items = @() + foreach ($part in $val.Split(";;")) { + $trimmed = $part.Trim() + if ($trimmed) { $items += $trimmed } + } + return ,$items +} + +# --- LocalString properties --- +$mlProps = @("Synonym","BriefInformation","DetailedInformation","Copyright","VendorInformationAddress","ConfigurationInformationAddress") +# Scalar properties +$scalarProps = @("Name","Version","Vendor","Comment","NamePrefix","UpdateCatalogAddress") +# Ref properties +$refProps = @("DefaultLanguage") + +# --- Operation: modify-property --- +function Do-ModifyProperty([string]$batchVal) { + $items = Parse-BatchValue $batchVal + foreach ($item in $items) { + $eqIdx = $item.IndexOf("=") + if ($eqIdx -lt 1) { + Write-Error "Invalid property format '$item', expected 'Key=Value'" + exit 1 + } + $propName = $item.Substring(0, $eqIdx).Trim() + $propValue = $item.Substring($eqIdx + 1).Trim() + + # Find property element + $propEl = $null + foreach ($child in $script:propsEl.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $propName) { + $propEl = $child; break + } + } + if (-not $propEl) { + Write-Error "Property '$propName' not found in Properties" + exit 1 + } + + if ($mlProps -contains $propName) { + # LocalString + if (-not $propValue) { + $propEl.InnerXml = "" + } else { + $indent = Get-ChildIndent $script:propsEl + $escaped = [System.Security.SecurityElement]::Escape($propValue) + $mlXml = "`r`n$indent`t`r`n$indent`t`tru`r`n$indent`t`t$escaped`r`n$indent`t`r`n$indent" + $propEl.InnerXml = $mlXml + } + } elseif ($scalarProps -contains $propName -or $refProps -contains $propName) { + # Simple text + if (-not $propValue) { $propEl.InnerXml = "" } + else { $propEl.InnerText = $propValue } + } else { + # Enum or other — just set text + $propEl.InnerText = $propValue + } + + $script:modifyCount++ + Info "Set $propName = `"$propValue`"" + } +} + +# --- Operation: add-childObject --- +function Do-AddChildObject([string]$batchVal) { + if (-not $script:childObjsEl) { Write-Error "No element found"; exit 1 } + + $items = Parse-BatchValue $batchVal + $cfgIndent = Get-ChildIndent $script:cfgEl + + # Expand self-closing if needed + if (-not $script:childObjsEl.HasChildNodes -or $script:childObjsEl.IsEmpty) { + Expand-SelfClosingElement $script:childObjsEl $cfgIndent + } + $childIndent = Get-ChildIndent $script:childObjsEl + + foreach ($item in $items) { + $dotIdx = $item.IndexOf(".") + if ($dotIdx -lt 1) { + Write-Error "Invalid format '$item', expected 'Type.Name'" + exit 1 + } + $typeName = $item.Substring(0, $dotIdx) + $objNameVal = $item.Substring($dotIdx + 1) + + # Check type is valid + $typeIdx = $script:typeOrder.IndexOf($typeName) + if ($typeIdx -lt 0) { + Write-Error "Unknown type '$typeName'" + exit 1 + } + + # Dedup check + $existing = $false + foreach ($child in $script:childObjsEl.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $typeName -and $child.InnerText -eq $objNameVal) { + $existing = $true; break + } + } + if ($existing) { + Warn "Already exists: $typeName.$objNameVal" + continue + } + + # Find insertion point: after last element of same type, or after last element of preceding type + $insertBefore = $null + $lastSameType = $null + $lastPrecedingType = $null + $currentTypeIdx = -1 + + foreach ($child in $script:childObjsEl.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + $childTypeIdx = $script:typeOrder.IndexOf($child.LocalName) + if ($childTypeIdx -lt 0) { continue } + + if ($child.LocalName -eq $typeName) { + # Same type — check alphabetical order + if ($child.InnerText -gt $objNameVal -and -not $insertBefore) { + # Insert before this element (alphabetical) + $insertBefore = $child + } + $lastSameType = $child + } elseif ($childTypeIdx -lt $typeIdx) { + $lastPrecedingType = $child + } elseif ($childTypeIdx -gt $typeIdx -and -not $insertBefore) { + # First element of a later type — insert before it + $insertBefore = $child + } + } + + # Create element + $newEl = $script:xmlDoc.CreateElement($typeName, $script:mdNs) + $newEl.InnerText = $objNameVal + + if ($insertBefore) { + Insert-BeforeElement $script:childObjsEl $newEl $insertBefore $childIndent + } else { + # Append at end (or after last same/preceding type) + Insert-BeforeElement $script:childObjsEl $newEl $null $childIndent + } + + $script:addCount++ + Info "Added: $typeName.$objNameVal" + } +} + +# --- Operation: remove-childObject --- +function Do-RemoveChildObject([string]$batchVal) { + if (-not $script:childObjsEl) { Write-Error "No element found"; exit 1 } + + $items = Parse-BatchValue $batchVal + foreach ($item in $items) { + $dotIdx = $item.IndexOf(".") + if ($dotIdx -lt 1) { + Write-Error "Invalid format '$item', expected 'Type.Name'" + exit 1 + } + $typeName = $item.Substring(0, $dotIdx) + $objNameVal = $item.Substring($dotIdx + 1) + + $found = $false + foreach ($child in @($script:childObjsEl.ChildNodes)) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $typeName -and $child.InnerText -eq $objNameVal) { + Remove-NodeWithWhitespace $child + $script:removeCount++ + Info "Removed: $typeName.$objNameVal" + $found = $true + break + } + } + if (-not $found) { Warn "Not found: $typeName.$objNameVal" } + } +} + +# --- Operation: add-defaultRole --- +function Do-AddDefaultRole([string]$batchVal) { + $items = Parse-BatchValue $batchVal + + # Find DefaultRoles element + $rolesEl = $null + foreach ($child in $script:propsEl.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "DefaultRoles") { + $rolesEl = $child; break + } + } + if (-not $rolesEl) { Write-Error "No element found in Properties"; exit 1 } + + $propsIndent = Get-ChildIndent $script:propsEl + if (-not $rolesEl.HasChildNodes -or $rolesEl.IsEmpty) { + Expand-SelfClosingElement $rolesEl $propsIndent + } + $roleIndent = Get-ChildIndent $rolesEl + + foreach ($item in $items) { + $roleName = $item + if (-not $roleName.StartsWith("Role.")) { $roleName = "Role.$roleName" } + + # Dedup + $existing = $false + foreach ($child in $rolesEl.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.InnerText.Trim() -eq $roleName) { + $existing = $true; break + } + } + if ($existing) { + Warn "DefaultRole already exists: $roleName" + continue + } + + $fragXml = "$roleName" + $nodes = Import-Fragment $fragXml + if ($nodes.Count -gt 0) { + Insert-BeforeElement $rolesEl $nodes[0] $null $roleIndent + $script:addCount++ + Info "Added DefaultRole: $roleName" + } + } +} + +# --- Operation: remove-defaultRole --- +function Do-RemoveDefaultRole([string]$batchVal) { + $items = Parse-BatchValue $batchVal + + $rolesEl = $null + foreach ($child in $script:propsEl.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "DefaultRoles") { + $rolesEl = $child; break + } + } + if (-not $rolesEl) { Write-Error "No element found"; exit 1 } + + foreach ($item in $items) { + $roleName = $item + if (-not $roleName.StartsWith("Role.")) { $roleName = "Role.$roleName" } + + $found = $false + foreach ($child in @($rolesEl.ChildNodes)) { + if ($child.NodeType -eq 'Element' -and $child.InnerText.Trim() -eq $roleName) { + Remove-NodeWithWhitespace $child + $script:removeCount++ + Info "Removed DefaultRole: $roleName" + $found = $true + break + } + } + if (-not $found) { Warn "DefaultRole not found: $roleName" } + } +} + +# --- Operation: set-defaultRoles --- +function Do-SetDefaultRoles([string]$batchVal) { + $items = Parse-BatchValue $batchVal + + $rolesEl = $null + foreach ($child in $script:propsEl.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "DefaultRoles") { + $rolesEl = $child; break + } + } + if (-not $rolesEl) { Write-Error "No element found"; exit 1 } + + # Clear all existing children + while ($rolesEl.HasChildNodes) { + $rolesEl.RemoveChild($rolesEl.FirstChild) | Out-Null + } + + if ($items.Count -eq 0) { + $script:modifyCount++ + Info "Cleared DefaultRoles" + return + } + + $propsIndent = Get-ChildIndent $script:propsEl + $roleIndent = "$propsIndent`t" + + # Add closing whitespace + $closeWs = $script:xmlDoc.CreateWhitespace("`r`n$propsIndent") + $rolesEl.AppendChild($closeWs) | Out-Null + + foreach ($item in $items) { + $roleName = $item + if (-not $roleName.StartsWith("Role.")) { $roleName = "Role.$roleName" } + + $fragXml = "$roleName" + $nodes = Import-Fragment $fragXml + if ($nodes.Count -gt 0) { + Insert-BeforeElement $rolesEl $nodes[0] $null $roleIndent + } + } + + $script:modifyCount++ + Info "Set DefaultRoles: $($items.Count) roles" +} + +# --- Execute operations --- +$operations = @() +if ($DefinitionFile) { + if (-not [System.IO.Path]::IsPathRooted($DefinitionFile)) { + $DefinitionFile = Join-Path (Get-Location).Path $DefinitionFile + } + $jsonText = Get-Content -Raw -Encoding UTF8 $DefinitionFile + $ops = $jsonText | ConvertFrom-Json + if ($ops -is [System.Array]) { + foreach ($op in $ops) { $operations += $op } + } else { + $operations += $ops + } +} else { + $operations += @{ operation = $Operation; value = $Value } +} + +foreach ($op in $operations) { + $opName = if ($op.operation) { "$($op.operation)" } else { "$Operation" } + $opValue = if ($op.value) { "$($op.value)" } else { "$Value" } + + switch ($opName) { + "modify-property" { Do-ModifyProperty $opValue } + "add-childObject" { Do-AddChildObject $opValue } + "remove-childObject" { Do-RemoveChildObject $opValue } + "add-defaultRole" { Do-AddDefaultRole $opValue } + "remove-defaultRole" { Do-RemoveDefaultRole $opValue } + "set-defaultRoles" { Do-SetDefaultRoles $opValue } + default { Write-Error "Unknown operation: $opName"; exit 1 } + } +} + +# --- Save --- +$settings = New-Object System.Xml.XmlWriterSettings +$settings.Encoding = New-Object System.Text.UTF8Encoding($true) +$settings.Indent = $false +$settings.NewLineHandling = [System.Xml.NewLineHandling]::None + +$memStream = New-Object System.IO.MemoryStream +$writer = [System.Xml.XmlWriter]::Create($memStream, $settings) +$script:xmlDoc.Save($writer) +$writer.Flush(); $writer.Close() + +$bytes = $memStream.ToArray() +$memStream.Close() +$text = [System.Text.Encoding]::UTF8.GetString($bytes) +if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) } +$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"') + +$utf8Bom = New-Object System.Text.UTF8Encoding($true) +[System.IO.File]::WriteAllText($resolvedPath, $text, $utf8Bom) +Info "Saved: $resolvedPath" + +# --- Auto-validate --- +if (-not $NoValidate) { + $validateScript = Join-Path (Join-Path $PSScriptRoot "..\..\cf-validate") "scripts\cf-validate.ps1" + $validateScript = [System.IO.Path]::GetFullPath($validateScript) + if (Test-Path $validateScript) { + Write-Host "" + Write-Host "--- Running cf-validate ---" + & powershell.exe -NoProfile -File $validateScript -ConfigPath $resolvedPath + } +} + +# --- Summary --- +Write-Host "" +Write-Host "=== cf-edit summary ===" +Write-Host " Configuration: $($script:objName)" +Write-Host " Added: $($script:addCount)" +Write-Host " Removed: $($script:removeCount)" +Write-Host " Modified: $($script:modifyCount)" +exit 0 diff --git a/.claude/skills/cf-info/SKILL.md b/.claude/skills/cf-info/SKILL.md new file mode 100644 index 00000000..35395126 --- /dev/null +++ b/.claude/skills/cf-info/SKILL.md @@ -0,0 +1,50 @@ +--- +name: cf-info +description: Анализ структуры конфигурации 1С — свойства, состав, счётчики объектов. Используй для обзора конфигурации — какие объекты есть, сколько их, какие настройки +argument-hint: [-Mode overview|brief|full] +allowed-tools: + - Bash + - Read + - Glob +--- + +# /cf-info — Структура конфигурации 1С + +Читает Configuration.xml из выгрузки конфигурации и выводит компактное описание структуры. + +## Параметры и команда + +| Параметр | Описание | +|----------|----------| +| `ConfigPath` | Путь к Configuration.xml или каталогу выгрузки | +| `Mode` | Режим: `overview` (default), `brief`, `full` | +| `Limit` / `Offset` | Пагинация (по умолчанию 150 строк) | +| `OutFile` | Записать результат в файл (UTF-8 BOM) | + +```powershell +powershell.exe -NoProfile -File .claude\skills\cf-info\scripts\cf-info.ps1 -ConfigPath "<путь>" +``` + +## Три режима + +| Режим | Что показывает | +|---|---| +| `overview` *(default)* | Заголовок + ключевые свойства + таблица счётчиков объектов по типам | +| `brief` | Одна строка: Имя — "Синоним" vВерсия \| N объектов \| совместимость | +| `full` | Все свойства по категориям + полный список ChildObjects + DefaultRoles + мобильные функциональности | + +## Примеры + +```powershell +# Обзор пустой конфигурации +... -ConfigPath upload/cfempty + +# Краткая сводка реальной конфигурации +... -ConfigPath upload/acc_8.3.24 -Mode brief + +# Полная информация +... -ConfigPath upload/acc_8.3.24 -Mode full + +# С пагинацией +... -ConfigPath upload/acc_8.3.24 -Mode full -Limit 50 -Offset 100 +``` diff --git a/.claude/skills/cf-info/scripts/cf-info.ps1 b/.claude/skills/cf-info/scripts/cf-info.ps1 new file mode 100644 index 00000000..c2a9e6fc --- /dev/null +++ b/.claude/skills/cf-info/scripts/cf-info.ps1 @@ -0,0 +1,387 @@ +# cf-info v1.0 — Compact summary of 1C configuration root +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory=$true)][string]$ConfigPath, + [ValidateSet("overview","brief","full")] + [string]$Mode = "overview", + [int]$Limit = 150, + [int]$Offset = 0, + [string]$OutFile +) + +$ErrorActionPreference = 'Stop' +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Output helper (always collect, paginate at the end) --- +$script:lines = @() +function Out([string]$text) { $script:lines += $text } + +# --- Resolve path --- +if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) { + $ConfigPath = Join-Path (Get-Location).Path $ConfigPath +} + +# Directory -> find Configuration.xml +if (Test-Path $ConfigPath -PathType Container) { + $candidate = Join-Path $ConfigPath "Configuration.xml" + if (Test-Path $candidate) { + $ConfigPath = $candidate + } else { + Write-Host "[ERROR] No Configuration.xml found in directory: $ConfigPath" + exit 1 + } +} + +if (-not (Test-Path $ConfigPath)) { + Write-Host "[ERROR] File not found: $ConfigPath" + exit 1 +} + +# --- Load XML --- +[xml]$xmlDoc = Get-Content -Path $ConfigPath -Encoding UTF8 +$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) +$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") +$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") +$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") +$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance") +$ns.AddNamespace("xs", "http://www.w3.org/2001/XMLSchema") +$ns.AddNamespace("app", "http://v8.1c.ru/8.2/managed-application/core") + +$mdRoot = $xmlDoc.SelectSingleNode("/md:MetaDataObject", $ns) +if (-not $mdRoot) { + Write-Host "[ERROR] Not a valid 1C metadata XML file (no MetaDataObject root)" + exit 1 +} + +$cfgNode = $mdRoot.SelectSingleNode("md:Configuration", $ns) +if (-not $cfgNode) { + Write-Host "[ERROR] No element found" + exit 1 +} + +$version = $mdRoot.GetAttribute("version") +$propsNode = $cfgNode.SelectSingleNode("md:Properties", $ns) +$childObjNode = $cfgNode.SelectSingleNode("md:ChildObjects", $ns) + +# --- Helpers --- +function Get-MLText($node) { + if (-not $node) { return "" } + $item = $node.SelectSingleNode("v8:item/v8:content", $ns) + if ($item -and $item.InnerText) { return $item.InnerText } + return "" +} + +function Get-PropText([string]$propName) { + $n = $propsNode.SelectSingleNode("md:$propName", $ns) + if ($n -and $n.InnerText) { return $n.InnerText } + return "" +} + +function Get-PropML([string]$propName) { + $n = $propsNode.SelectSingleNode("md:$propName", $ns) + return (Get-MLText $n) +} + +# --- Type name maps (canonical order, 44 types) --- +$typeOrder = @( + "Language","Subsystem","StyleItem","Style", + "CommonPicture","SessionParameter","Role","CommonTemplate", + "FilterCriterion","CommonModule","CommonAttribute","ExchangePlan", + "XDTOPackage","WebService","HTTPService","WSReference", + "EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption", + "FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup", + "Constant","CommonForm","Catalog","Document", + "DocumentNumerator","Sequence","DocumentJournal","Enum", + "Report","DataProcessor","InformationRegister","AccumulationRegister", + "ChartOfCharacteristicTypes","ChartOfAccounts","AccountingRegister", + "ChartOfCalculationTypes","CalculationRegister", + "BusinessProcess","Task","IntegrationService" +) + +$typeRuNames = @{ + "Language"="Языки"; "Subsystem"="Подсистемы"; "StyleItem"="Элементы стиля"; "Style"="Стили" + "CommonPicture"="Общие картинки"; "SessionParameter"="Параметры сеанса"; "Role"="Роли" + "CommonTemplate"="Общие макеты"; "FilterCriterion"="Критерии отбора"; "CommonModule"="Общие модули" + "CommonAttribute"="Общие реквизиты"; "ExchangePlan"="Планы обмена"; "XDTOPackage"="XDTO-пакеты" + "WebService"="Веб-сервисы"; "HTTPService"="HTTP-сервисы"; "WSReference"="WS-ссылки" + "EventSubscription"="Подписки на события"; "ScheduledJob"="Регламентные задания" + "SettingsStorage"="Хранилища настроек"; "FunctionalOption"="Функциональные опции" + "FunctionalOptionsParameter"="Параметры ФО"; "DefinedType"="Определяемые типы" + "CommonCommand"="Общие команды"; "CommandGroup"="Группы команд"; "Constant"="Константы" + "CommonForm"="Общие формы"; "Catalog"="Справочники"; "Document"="Документы" + "DocumentNumerator"="Нумераторы"; "Sequence"="Последовательности"; "DocumentJournal"="Журналы документов" + "Enum"="Перечисления"; "Report"="Отчёты"; "DataProcessor"="Обработки" + "InformationRegister"="Регистры сведений"; "AccumulationRegister"="Регистры накопления" + "ChartOfCharacteristicTypes"="ПВХ"; "ChartOfAccounts"="Планы счетов" + "AccountingRegister"="Регистры бухгалтерии"; "ChartOfCalculationTypes"="ПВР" + "CalculationRegister"="Регистры расчёта"; "BusinessProcess"="Бизнес-процессы" + "Task"="Задачи"; "IntegrationService"="Сервисы интеграции" +} + +# --- Count objects in ChildObjects --- +$objectCounts = [ordered]@{} +$totalObjects = 0 + +if ($childObjNode) { + foreach ($child in $childObjNode.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + $typeName = $child.LocalName + if (-not $objectCounts.Contains($typeName)) { + $objectCounts[$typeName] = 0 + } + $objectCounts[$typeName] = $objectCounts[$typeName] + 1 + $totalObjects++ + } +} + +# --- Read key properties --- +$cfgName = Get-PropText "Name" +$cfgSynonym = Get-PropML "Synonym" +$cfgVersion = Get-PropText "Version" +$cfgVendor = Get-PropText "Vendor" +$cfgCompat = Get-PropText "CompatibilityMode" +$cfgExtCompat = Get-PropText "ConfigurationExtensionCompatibilityMode" +$cfgDefaultRun = Get-PropText "DefaultRunMode" +$cfgScript = Get-PropText "ScriptVariant" +$cfgDefaultLang = Get-PropText "DefaultLanguage" +$cfgDataLock = Get-PropText "DataLockControlMode" +$dash = [char]0x2014 +$cfgModality = Get-PropText "ModalityUseMode" +$cfgIntfCompat = Get-PropText "InterfaceCompatibilityMode" +$cfgAutoNum = Get-PropText "ObjectAutonumerationMode" +$cfgSyncCalls = Get-PropText "SynchronousPlatformExtensionAndAddInCallUseMode" +$cfgDbSpaces = Get-PropText "DatabaseTablespacesUseMode" +$cfgWindowMode = Get-PropText "MainClientApplicationWindowMode" + +# --- BRIEF mode --- +if ($Mode -eq "brief") { + $synPart = if ($cfgSynonym) { " $dash `"$cfgSynonym`"" } else { "" } + $verPart = if ($cfgVersion) { " v$cfgVersion" } else { "" } + $compatPart = if ($cfgCompat) { " | $cfgCompat" } else { "" } + Out "Конфигурация: ${cfgName}${synPart}${verPart} | $totalObjects объектов${compatPart}" +} + +# --- OVERVIEW mode --- +if ($Mode -eq "overview") { + $synPart = if ($cfgSynonym) { " $dash `"$cfgSynonym`"" } else { "" } + $verPart = if ($cfgVersion) { " v$cfgVersion" } else { "" } + Out "=== Конфигурация: ${cfgName}${synPart}${verPart} ===" + Out "" + + # Key properties + Out "Формат: $version" + if ($cfgVendor) { Out "Поставщик: $cfgVendor" } + if ($cfgVersion) { Out "Версия: $cfgVersion" } + Out "Совместимость: $cfgCompat" + Out "Режим запуска: $cfgDefaultRun" + Out "Язык скриптов: $cfgScript" + Out "Язык: $cfgDefaultLang" + Out "Блокировки: $cfgDataLock" + Out "Модальность: $cfgModality" + Out "Интерфейс: $cfgIntfCompat" + Out "" + + # Object counts table + Out "--- Состав ($totalObjects объектов) ---" + Out "" + $maxTypeLen = 0 + foreach ($typeName in $typeOrder) { + if ($objectCounts.Contains($typeName)) { + $ruName = $typeRuNames[$typeName] + if ($ruName.Length -gt $maxTypeLen) { $maxTypeLen = $ruName.Length } + } + } + if ($maxTypeLen -lt 10) { $maxTypeLen = 10 } + + foreach ($typeName in $typeOrder) { + if ($objectCounts.Contains($typeName)) { + $count = $objectCounts[$typeName] + $ruName = $typeRuNames[$typeName] + $padded = $ruName.PadRight($maxTypeLen) + Out " $padded $count" + } + } +} + +# --- FULL mode --- +if ($Mode -eq "full") { + $synPart = if ($cfgSynonym) { " $dash `"$cfgSynonym`"" } else { "" } + $verPart = if ($cfgVersion) { " v$cfgVersion" } else { "" } + Out "=== Конфигурация: ${cfgName}${synPart}${verPart} ===" + Out "" + + # --- Section: Identification --- + Out "--- Идентификация ---" + Out "UUID: $($cfgNode.GetAttribute('uuid'))" + Out "Имя: $cfgName" + if ($cfgSynonym) { Out "Синоним: $cfgSynonym" } + $cfgComment = Get-PropText "Comment" + if ($cfgComment) { Out "Комментарий: $cfgComment" } + $cfgPrefix = Get-PropText "NamePrefix" + if ($cfgPrefix) { Out "Префикс: $cfgPrefix" } + if ($cfgVendor) { Out "Поставщик: $cfgVendor" } + if ($cfgVersion) { Out "Версия: $cfgVersion" } + $cfgUpdateAddr = Get-PropText "UpdateCatalogAddress" + if ($cfgUpdateAddr) { Out "Каталог обн.: $cfgUpdateAddr" } + Out "" + + # --- Section: Modes --- + Out "--- Режимы работы ---" + Out "Формат: $version" + Out "Совместимость: $cfgCompat" + Out "Совм. расширений: $cfgExtCompat" + Out "Режим запуска: $cfgDefaultRun" + Out "Язык скриптов: $cfgScript" + Out "Блокировки: $cfgDataLock" + Out "Автонумерация: $cfgAutoNum" + Out "Модальность: $cfgModality" + Out "Синхр. вызовы: $cfgSyncCalls" + Out "Интерфейс: $cfgIntfCompat" + Out "Табл. пространства: $cfgDbSpaces" + Out "Режим окна: $cfgWindowMode" + Out "" + + # --- Section: Language, roles, purposes --- + Out "--- Назначение ---" + Out "Язык по умолч.: $cfgDefaultLang" + + # UsePurposes + $purposeNode = $propsNode.SelectSingleNode("md:UsePurposes", $ns) + if ($purposeNode) { + $purposes = @() + foreach ($val in $purposeNode.SelectNodes("v8:Value", $ns)) { + $purposes += $val.InnerText + } + if ($purposes.Count -gt 0) { Out "Назначения: $($purposes -join ', ')" } + } + + # DefaultRoles + $rolesNode = $propsNode.SelectSingleNode("md:DefaultRoles", $ns) + if ($rolesNode) { + $roles = @() + foreach ($item in $rolesNode.SelectNodes("xr:Item", $ns)) { + $roles += $item.InnerText + } + if ($roles.Count -gt 0) { + Out "Роли по умолч.: $($roles.Count)" + foreach ($r in $roles) { Out " - $r" } + } + } + + # Booleans + $useMF = Get-PropText "UseManagedFormInOrdinaryApplication" + $useOF = Get-PropText "UseOrdinaryFormInManagedApplication" + Out "Управл.формы в обычн.: $useMF" + Out "Обычн.формы в управл.: $useOF" + Out "" + + # --- Section: Storages & default forms --- + Out "--- Хранилища и формы по умолчанию ---" + $storageProps = @("CommonSettingsStorage","ReportsUserSettingsStorage","ReportsVariantsStorage","FormDataSettingsStorage","DynamicListsUserSettingsStorage","URLExternalDataStorage") + foreach ($sp in $storageProps) { + $val = Get-PropText $sp + if ($val) { Out " ${sp}: $val" } + } + $formProps = @("DefaultReportForm","DefaultReportVariantForm","DefaultReportSettingsForm","DefaultReportAppearanceTemplate","DefaultDynamicListSettingsForm","DefaultSearchForm","DefaultDataHistoryChangeHistoryForm","DefaultDataHistoryVersionDataForm","DefaultDataHistoryVersionDifferencesForm","DefaultCollaborationSystemUsersChoiceForm","DefaultConstantsForm","DefaultInterface","DefaultStyle") + foreach ($fp in $formProps) { + $val = Get-PropText $fp + if ($val) { Out " ${fp}: $val" } + } + Out "" + + # --- Section: Info --- + $cfgBrief = Get-PropML "BriefInformation" + $cfgDetail = Get-PropML "DetailedInformation" + $cfgCopyright = Get-PropML "Copyright" + $cfgVendorAddr = Get-PropML "VendorInformationAddress" + $cfgInfoAddr = Get-PropML "ConfigurationInformationAddress" + if ($cfgBrief -or $cfgDetail -or $cfgCopyright -or $cfgVendorAddr -or $cfgInfoAddr) { + Out "--- Информация ---" + if ($cfgBrief) { Out "Краткая: $cfgBrief" } + if ($cfgDetail) { Out "Подробная: $cfgDetail" } + if ($cfgCopyright) { Out "Copyright: $cfgCopyright" } + if ($cfgVendorAddr) { Out "Сайт поставщика: $cfgVendorAddr" } + if ($cfgInfoAddr) { Out "Адрес информ.: $cfgInfoAddr" } + Out "" + } + + # --- Section: Mobile functionalities --- + $mobileFunc = $propsNode.SelectSingleNode("md:UsedMobileApplicationFunctionalities", $ns) + if ($mobileFunc) { + $enabledFuncs = @() + $disabledFuncs = @() + foreach ($func in $mobileFunc.SelectNodes("app:functionality", $ns)) { + $fName = $func.SelectSingleNode("app:functionality", $ns) + $fUse = $func.SelectSingleNode("app:use", $ns) + if ($fName -and $fUse) { + if ($fUse.InnerText -eq "true") { + $enabledFuncs += $fName.InnerText + } else { + $disabledFuncs += $fName.InnerText + } + } + } + $totalFunc = $enabledFuncs.Count + $disabledFuncs.Count + Out "--- Мобильные функциональности ($totalFunc, включено: $($enabledFuncs.Count)) ---" + if ($enabledFuncs.Count -gt 0) { + foreach ($f in $enabledFuncs) { Out " [+] $f" } + } + foreach ($f in $disabledFuncs) { Out " [-] $f" } + Out "" + } + + # --- Section: InternalInfo --- + $internalInfo = $cfgNode.SelectSingleNode("md:InternalInfo", $ns) + if ($internalInfo) { + $contained = $internalInfo.SelectNodes("xr:ContainedObject", $ns) + Out "--- InternalInfo ($($contained.Count) ContainedObject) ---" + foreach ($co in $contained) { + $classId = $co.SelectSingleNode("xr:ClassId", $ns).InnerText + $objectId = $co.SelectSingleNode("xr:ObjectId", $ns).InnerText + Out " $classId -> $objectId" + } + Out "" + } + + # --- Section: ChildObjects (full list) --- + Out "--- Состав ($totalObjects объектов) ---" + Out "" + + foreach ($typeName in $typeOrder) { + if (-not $objectCounts.Contains($typeName)) { continue } + $count = $objectCounts[$typeName] + $ruName = $typeRuNames[$typeName] + Out " $ruName ($typeName): $count" + + # Collect names for this type + $names = @() + foreach ($child in $childObjNode.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $typeName) { + $names += $child.InnerText + } + } + foreach ($n in $names) { Out " $n" } + } +} + +# --- Pagination and output --- +$total = $script:lines.Count +if ($Offset -gt 0 -or $Limit -lt $total) { + $start = [Math]::Min($Offset, $total) + $end = [Math]::Min($start + $Limit, $total) + $page = $script:lines[$start..($end - 1)] + $result = ($page -join "`n") + if ($end -lt $total) { + $result += "`n`n... ($end of $total lines, use -Offset $end to continue)" + } +} else { + $result = ($script:lines -join "`n") +} + +Write-Host $result + +if ($OutFile) { + $utf8Bom = New-Object System.Text.UTF8Encoding $true + [System.IO.File]::WriteAllText($OutFile, $result, $utf8Bom) + Write-Host "`nWritten to: $OutFile" +} diff --git a/.claude/skills/cf-init/SKILL.md b/.claude/skills/cf-init/SKILL.md new file mode 100644 index 00000000..da1bf00c --- /dev/null +++ b/.claude/skills/cf-init/SKILL.md @@ -0,0 +1,58 @@ +--- +name: cf-init +description: Создать пустую конфигурацию 1С (scaffold XML-исходников). Используй когда нужно начать новую конфигурацию с нуля +argument-hint: [-Synonym ] [-OutputDir src] +allowed-tools: + - Bash + - Read + - Glob +--- + +# /cf-init — Создание пустой конфигурации 1С + +Создаёт scaffold исходников пустой конфигурации 1С: `Configuration.xml`, `Languages/Русский.xml`. + +## Параметры и команда + +| Параметр | Описание | +|----------|----------| +| `Name` | Имя конфигурации (обязат.) | +| `Synonym` | Синоним (= Name если не указан) | +| `OutputDir` | Каталог для создания (default: `src`) | +| `Version` | Версия конфигурации | +| `Vendor` | Поставщик | +| `CompatibilityMode` | Режим совместимости (default: `Version8_3_24`) | + +```powershell +powershell.exe -NoProfile -File .claude\skills\cf-init\scripts\cf-init.ps1 -Name "МояКонфигурация" +``` + +## Что создаётся + +``` +/ +├── Configuration.xml # Корневой файл — все свойства +└── Languages/ + └── Русский.xml # Язык по умолчанию +``` + +## Примеры + +```powershell +# Базовая конфигурация +... -Name МояКонфигурация -Synonym "Моя конфигурация" -OutputDir test-tmp/cf + +# С версией и поставщиком +... -Name TestCfg -Synonym "Тестовая" -Version "1.0.0.1" -Vendor "Фирма 1С" -OutputDir test-tmp/cf2 + +# Другой режим совместимости +... -Name TestCfg -CompatibilityMode Version8_3_27 -OutputDir test-tmp/cf3 +``` + +## Верификация + +``` +/cf-init TestConfig -OutputDir test-tmp/cf +/cf-info test-tmp/cf — проверить созданное +/cf-validate test-tmp/cf — валидировать +``` diff --git a/.claude/skills/cf-init/scripts/cf-init.ps1 b/.claude/skills/cf-init/scripts/cf-init.ps1 new file mode 100644 index 00000000..bce9c427 --- /dev/null +++ b/.claude/skills/cf-init/scripts/cf-init.ps1 @@ -0,0 +1,215 @@ +# cf-init v1.0 — Create empty 1C configuration scaffold +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)] + [string]$Name, + [string]$Synonym = $Name, + [string]$OutputDir = "src", + [string]$Version, + [string]$Vendor, + [string]$CompatibilityMode = "Version8_3_24" +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve output dir --- +if (-not [System.IO.Path]::IsPathRooted($OutputDir)) { + $OutputDir = Join-Path (Get-Location).Path $OutputDir +} + +# --- Check existing --- +$cfgFile = Join-Path $OutputDir "Configuration.xml" +if (Test-Path $cfgFile) { + Write-Error "Configuration.xml already exists: $cfgFile" + exit 1 +} + +# --- Generate UUIDs --- +$uuidCfg = [guid]::NewGuid().ToString() +$uuidLang = [guid]::NewGuid().ToString() +# 7 ContainedObject ObjectIds +$co1 = [guid]::NewGuid().ToString() +$co2 = [guid]::NewGuid().ToString() +$co3 = [guid]::NewGuid().ToString() +$co4 = [guid]::NewGuid().ToString() +$co5 = [guid]::NewGuid().ToString() +$co6 = [guid]::NewGuid().ToString() +$co7 = [guid]::NewGuid().ToString() + +# --- Mobile functionalities --- +$mobileFuncs = @( + @("Biometrics","true"), @("Location","false"), @("BackgroundLocation","false"), + @("BluetoothPrinters","false"), @("WiFiPrinters","false"), @("Contacts","false"), + @("Calendars","false"), @("PushNotifications","false"), @("LocalNotifications","false"), + @("InAppPurchases","false"), @("PersonalComputerFileExchange","false"), @("Ads","false"), + @("NumberDialing","false"), @("CallProcessing","false"), @("CallLog","false"), + @("AutoSendSMS","false"), @("ReceiveSMS","false"), @("SMSLog","false"), + @("Camera","false"), @("Microphone","false"), @("MusicLibrary","false"), + @("PictureAndVideoLibraries","false"), @("AudioPlaybackAndVibration","false"), + @("BackgroundAudioPlaybackAndVibration","false"), @("InstallPackages","false"), + @("OSBackup","true"), @("ApplicationUsageStatistics","false"), + @("BarcodeScanning","false"), @("BackgroundAudioRecording","false"), + @("AllFilesAccess","false"), @("Videoconferences","false"), @("NFC","false"), + @("DocumentScanning","false"), @("SpeechToText","false"), @("Geofences","false"), + @("IncomingShareRequests","false"), @("AllIncomingShareRequestsTypesProcessing","false") +) + +$mobileXml = "" +foreach ($mf in $mobileFuncs) { + $mobileXml += "`r`n`t`t`t`t`r`n`t`t`t`t`t$($mf[0])`r`n`t`t`t`t`t$($mf[1])`r`n`t`t`t`t" +} + +# --- Synonym XML --- +$synonymXml = "" +if ($Synonym) { + $synonymXml = "`r`n`t`t`t`t`r`n`t`t`t`t`tru`r`n`t`t`t`t`t$([System.Security.SecurityElement]::Escape($Synonym))`r`n`t`t`t`t`r`n`t`t`t" +} + +# --- Optional properties --- +$vendorXml = if ($Vendor) { [System.Security.SecurityElement]::Escape($Vendor) } else { "" } +$versionXml = if ($Version) { [System.Security.SecurityElement]::Escape($Version) } else { "" } + +# --- Configuration.xml --- +$cfgXml = @" + + + + + + 9cd510cd-abfc-11d4-9434-004095e12fc7 + $co1 + + + 9fcd25a0-4822-11d4-9414-008048da11f9 + $co2 + + + e3687481-0a87-462c-a166-9f34594f9bba + $co3 + + + 9de14907-ec23-4a07-96f0-85521cb6b53b + $co4 + + + 51f2d5d8-ea4d-4064-8892-82951750031e + $co5 + + + e68182ea-4237-4383-967f-90c1e3370bc7 + $co6 + + + fb282519-d103-4dd3-bc12-cb271d631dfc + $co7 + + + + $([System.Security.SecurityElement]::Escape($Name)) + $synonymXml + + + $CompatibilityMode + ManagedApplication + + PlatformApplication + + Russian + + $vendorXml + $versionXml + + false + false + false + + + + + + + + + + + + + + + + + + + + $mobileXml + + + + + Normal + + + Language.Русский + + + + + + Managed + NotAutoFree + DontUse + DontUse + Taxi + DontUse + $CompatibilityMode + + + + Русский + + + +"@ + +# --- Languages/Русский.xml --- +$langXml = @" + + + + + Русский + + + ru + Русский + + + + ru + + + +"@ + +# --- Create directories --- +if (-not (Test-Path $OutputDir)) { + New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null +} +$langDir = Join-Path $OutputDir "Languages" +if (-not (Test-Path $langDir)) { + New-Item -ItemType Directory -Path $langDir -Force | Out-Null +} + +# --- Write files with UTF-8 BOM --- +$enc = New-Object System.Text.UTF8Encoding($true) + +[System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc) +$langFile = Join-Path $langDir "Русский.xml" +[System.IO.File]::WriteAllText($langFile, $langXml, $enc) + +# --- Output --- +Write-Host "[OK] Создана конфигурация: $Name" +Write-Host " Каталог: $OutputDir" +Write-Host " Configuration.xml: $cfgFile" +Write-Host " Languages: $langFile" diff --git a/.claude/skills/cf-validate/SKILL.md b/.claude/skills/cf-validate/SKILL.md new file mode 100644 index 00000000..ed9afd8a --- /dev/null +++ b/.claude/skills/cf-validate/SKILL.md @@ -0,0 +1,70 @@ +--- +name: cf-validate +description: Валидация конфигурации 1С. Используй после создания или модификации конфигурации для проверки корректности +argument-hint: [-MaxErrors 30] +allowed-tools: + - Bash + - Read + - Glob +--- + +# /cf-validate — валидация конфигурации 1С + +Проверяет Configuration.xml на структурные ошибки: XML well-formedness, InternalInfo, свойства, enum-значения, ChildObjects, DefaultLanguage, файлы языков, каталоги объектов. + +## Параметры и команда + +| Параметр | Описание | +|----------|----------| +| `ConfigPath` | Путь к Configuration.xml или каталогу выгрузки | +| `MaxErrors` | Остановиться после N ошибок (default: 30) | +| `OutFile` | Записать результат в файл (UTF-8 BOM) | + +```powershell +powershell.exe -NoProfile -File .claude\skills\cf-validate\scripts\cf-validate.ps1 -ConfigPath "<путь>" +``` + +## Выполняемые проверки + +| # | Проверка | Серьёзность | +|---|----------|-------------| +| 1 | XML well-formedness, MetaDataObject/Configuration, version 2.17/2.20 | ERROR | +| 2 | InternalInfo: 7 ContainedObject, валидные ClassId, уникальность | ERROR | +| 3 | Properties: Name непустой, Synonym, DefaultLanguage, DefaultRunMode | ERROR/WARN | +| 4 | Properties: enum-значения (11 свойств) | ERROR | +| 5 | ChildObjects: валидные имена типов (44 типа), нет дубликатов, порядок типов | ERROR/WARN | +| 6 | DefaultLanguage ссылается на существующий Language в ChildObjects | ERROR | +| 7 | Файлы языков Languages/.xml существуют | WARN | +| 8 | Каталоги объектов из ChildObjects существуют (spot-check) | WARN | + +## Вывод + +``` +=== Validation: Configuration.МояКонфигурация === + +[OK] 1. Root structure: MetaDataObject/Configuration, version 2.17 +[OK] 2. InternalInfo: 7 ContainedObject, all ClassIds valid +[OK] 3. Properties: Name="МояКонфигурация", Synonym present +[OK] 4. Property values: 11 enum properties checked +[OK] 5. ChildObjects: 1 types, 1 objects, order correct +[OK] 6. DefaultLanguage "Language.Русский" found in ChildObjects +[OK] 7. Language files: 1/1 exist +[OK] 8. Object directories: spot-check passed + +=== Result: 0 errors, 0 warnings === +``` + +Exit code: 0 = OK, 1 = errors. + +## Примеры + +```powershell +# Пустая конфигурация +... -ConfigPath upload/cfempty + +# Реальная конфигурация +... -ConfigPath C:\WS\tasks\cfsrc\acc_8.3.24 + +# С лимитом ошибок +... -ConfigPath test-tmp/cf -MaxErrors 10 +``` diff --git a/.claude/skills/cf-validate/scripts/cf-validate.ps1 b/.claude/skills/cf-validate/scripts/cf-validate.ps1 new file mode 100644 index 00000000..c0c6fed3 --- /dev/null +++ b/.claude/skills/cf-validate/scripts/cf-validate.ps1 @@ -0,0 +1,538 @@ +# cf-validate v1.0 — Validate 1C configuration root structure +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)] + [string]$ConfigPath, + + [int]$MaxErrors = 30, + + [string]$OutFile +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve path --- +if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) { + $ConfigPath = Join-Path (Get-Location).Path $ConfigPath +} + +if (Test-Path $ConfigPath -PathType Container) { + $candidate = Join-Path $ConfigPath "Configuration.xml" + if (Test-Path $candidate) { + $ConfigPath = $candidate + } else { + Write-Host "[ERROR] No Configuration.xml found in directory: $ConfigPath" + exit 1 + } +} + +if (-not (Test-Path $ConfigPath)) { + Write-Host "[ERROR] File not found: $ConfigPath" + exit 1 +} + +$resolvedPath = (Resolve-Path $ConfigPath).Path +$configDir = Split-Path $resolvedPath -Parent + +# --- Output infrastructure --- +$script:errors = 0 +$script:warnings = 0 +$script:stopped = $false +$script:output = New-Object System.Text.StringBuilder 8192 + +function Out-Line { + param([string]$msg) + $script:output.AppendLine($msg) | Out-Null +} + +function Report-OK { + param([string]$msg) + Out-Line "[OK] $msg" +} + +function Report-Error { + param([string]$msg) + $script:errors++ + Out-Line "[ERROR] $msg" + if ($script:errors -ge $MaxErrors) { + $script:stopped = $true + } +} + +function Report-Warn { + param([string]$msg) + $script:warnings++ + Out-Line "[WARN] $msg" +} + +$finalize = { + Out-Line "" + Out-Line "=== Result: $($script:errors) errors, $($script:warnings) warnings ===" + + $result = $script:output.ToString() + Write-Host $result + + if ($OutFile) { + $utf8Bom = New-Object System.Text.UTF8Encoding $true + [System.IO.File]::WriteAllText($OutFile, $result, $utf8Bom) + Write-Host "Written to: $OutFile" + } +} + +# --- Reference tables --- +$guidPattern = '^[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}$' +$identPattern = '^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_][A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$' + +# 7 fixed ClassIds for Configuration +$validClassIds = @( + "9cd510cd-abfc-11d4-9434-004095e12fc7", # managed application module + "9fcd25a0-4822-11d4-9414-008048da11f9", # ordinary application module + "e3687481-0a87-462c-a166-9f34594f9bba", # session module + "9de14907-ec23-4a07-96f0-85521cb6b53b", # external connection module + "51f2d5d8-ea4d-4064-8892-82951750031e", # command interface + "e68182ea-4237-4383-967f-90c1e3370bc7", # main section command interface + "fb282519-d103-4dd3-bc12-cb271d631dfc" # home page / client app interface +) + +# 44 types in canonical order +$childObjectTypes = @( + "Language","Subsystem","StyleItem","Style", + "CommonPicture","SessionParameter","Role","CommonTemplate", + "FilterCriterion","CommonModule","CommonAttribute","ExchangePlan", + "XDTOPackage","WebService","HTTPService","WSReference", + "EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption", + "FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup", + "Constant","CommonForm","Catalog","Document", + "DocumentNumerator","Sequence","DocumentJournal","Enum", + "Report","DataProcessor","InformationRegister","AccumulationRegister", + "ChartOfCharacteristicTypes","ChartOfAccounts","AccountingRegister", + "ChartOfCalculationTypes","CalculationRegister", + "BusinessProcess","Task","IntegrationService" +) + +# Type -> directory mapping +$childTypeDirMap = @{ + "Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles" + "CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles" + "CommonTemplate"="CommonTemplates"; "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules" + "CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"; "XDTOPackage"="XDTOPackages" + "WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences" + "EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs" + "SettingsStorage"="SettingsStorages"; "FunctionalOption"="FunctionalOptions" + "FunctionalOptionsParameter"="FunctionalOptionsParameters"; "DefinedType"="DefinedTypes" + "CommonCommand"="CommonCommands"; "CommandGroup"="CommandGroups"; "Constant"="Constants" + "CommonForm"="CommonForms"; "Catalog"="Catalogs"; "Document"="Documents" + "DocumentNumerator"="DocumentNumerators"; "Sequence"="Sequences" + "DocumentJournal"="DocumentJournals"; "Enum"="Enums"; "Report"="Reports" + "DataProcessor"="DataProcessors"; "InformationRegister"="InformationRegisters" + "AccumulationRegister"="AccumulationRegisters" + "ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes" + "ChartOfAccounts"="ChartsOfAccounts"; "AccountingRegister"="AccountingRegisters" + "ChartOfCalculationTypes"="ChartsOfCalculationTypes" + "CalculationRegister"="CalculationRegisters" + "BusinessProcess"="BusinessProcesses"; "Task"="Tasks" + "IntegrationService"="IntegrationServices" +} + +# Valid enum values for Configuration properties +$validEnumValues = @{ + "ConfigurationExtensionCompatibilityMode" = @("DontUse","Version8_1","Version8_2_13","Version8_2_16","Version8_3_1","Version8_3_2","Version8_3_3","Version8_3_4","Version8_3_5","Version8_3_6","Version8_3_7","Version8_3_8","Version8_3_9","Version8_3_10","Version8_3_11","Version8_3_12","Version8_3_13","Version8_3_14","Version8_3_15","Version8_3_16","Version8_3_17","Version8_3_18","Version8_3_19","Version8_3_20","Version8_3_21","Version8_3_22","Version8_3_23","Version8_3_24","Version8_3_25","Version8_3_26","Version8_3_27","Version8_3_28") + "DefaultRunMode" = @("ManagedApplication","OrdinaryApplication","Auto") + "ScriptVariant" = @("Russian","English") + "DataLockControlMode" = @("Automatic","Managed","AutomaticAndManaged") + "ObjectAutonumerationMode" = @("NotAutoFree","AutoFree") + "ModalityUseMode" = @("DontUse","Use","UseWithWarnings") + "SynchronousPlatformExtensionAndAddInCallUseMode" = @("DontUse","Use","UseWithWarnings") + "InterfaceCompatibilityMode" = @("Taxi","TaxiEnableVersion8_2","Version8_2") + "DatabaseTablespacesUseMode" = @("DontUse","Use") + "MainClientApplicationWindowMode" = @("Normal","Fullscreen","Kiosk") + "CompatibilityMode" = @("DontUse","Version8_1","Version8_2_13","Version8_2_16","Version8_3_1","Version8_3_2","Version8_3_3","Version8_3_4","Version8_3_5","Version8_3_6","Version8_3_7","Version8_3_8","Version8_3_9","Version8_3_10","Version8_3_11","Version8_3_12","Version8_3_13","Version8_3_14","Version8_3_15","Version8_3_16","Version8_3_17","Version8_3_18","Version8_3_19","Version8_3_20","Version8_3_21","Version8_3_22","Version8_3_23","Version8_3_24","Version8_3_25","Version8_3_26","Version8_3_27","Version8_3_28") +} + +# --- 1. Parse XML --- +Out-Line "" + +$xmlDoc = $null +try { + $xmlDoc = New-Object System.Xml.XmlDocument + $xmlDoc.PreserveWhitespace = $false + $xmlDoc.Load($resolvedPath) +} catch { + Out-Line "=== Validation: Configuration (parse failed) ===" + Out-Line "" + Report-Error "1. XML parse failed: $($_.Exception.Message)" + & $finalize + exit 1 +} + +# --- Register namespaces --- +$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) +$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") +$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") +$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") +$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance") +$ns.AddNamespace("xs", "http://www.w3.org/2001/XMLSchema") +$ns.AddNamespace("app", "http://v8.1c.ru/8.2/managed-application/core") + +$root = $xmlDoc.DocumentElement + +# --- Check 1: Root structure --- +$check1Ok = $true +$expectedNs = "http://v8.1c.ru/8.3/MDClasses" + +if ($root.LocalName -ne "MetaDataObject") { + Report-Error "1. Root element is '$($root.LocalName)', expected 'MetaDataObject'" + & $finalize + exit 1 +} + +if ($root.NamespaceURI -ne $expectedNs) { + Report-Error "1. Root namespace is '$($root.NamespaceURI)', expected '$expectedNs'" + $check1Ok = $false +} + +$version = $root.GetAttribute("version") +if (-not $version) { + Report-Warn "1. Missing version attribute on MetaDataObject" +} elseif ($version -ne "2.17" -and $version -ne "2.20") { + Report-Warn "1. Unusual version '$version' (expected 2.17 or 2.20)" +} + +# Must have Configuration child +$cfgNode = $null +foreach ($child in $root.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Configuration" -and $child.NamespaceURI -eq $expectedNs) { + $cfgNode = $child; break + } +} + +if (-not $cfgNode) { + Report-Error "1. No element found inside MetaDataObject" + & $finalize + exit 1 +} + +# UUID +$cfgUuid = $cfgNode.GetAttribute("uuid") +if (-not $cfgUuid) { + Report-Error "1. Missing uuid on " + $check1Ok = $false +} elseif ($cfgUuid -notmatch $guidPattern) { + Report-Error "1. Invalid uuid '$cfgUuid' on " + $check1Ok = $false +} + +# Get name early for header +$propsNode = $cfgNode.SelectSingleNode("md:Properties", $ns) +$nameNode = if ($propsNode) { $propsNode.SelectSingleNode("md:Name", $ns) } else { $null } +$objName = if ($nameNode -and $nameNode.InnerText) { $nameNode.InnerText } else { "(unknown)" } + +$script:output.Insert(0, "=== Validation: Configuration.$objName ===$([Environment]::NewLine)") | Out-Null + +if ($check1Ok) { + Report-OK "1. Root structure: MetaDataObject/Configuration, version $version" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 2: InternalInfo --- +$internalInfo = $cfgNode.SelectSingleNode("md:InternalInfo", $ns) +$check2Ok = $true + +if (-not $internalInfo) { + Report-Error "2. InternalInfo: missing" +} else { + $contained = $internalInfo.SelectNodes("xr:ContainedObject", $ns) + if ($contained.Count -ne 7) { + Report-Warn "2. InternalInfo: expected 7 ContainedObject, found $($contained.Count)" + } + + $foundClassIds = @{} + foreach ($co in $contained) { + $classId = $co.SelectSingleNode("xr:ClassId", $ns) + $objectId = $co.SelectSingleNode("xr:ObjectId", $ns) + + if (-not $classId -or -not $classId.InnerText) { + Report-Error "2. ContainedObject missing ClassId" + $check2Ok = $false + continue + } + + $cid = $classId.InnerText + if ($validClassIds -notcontains $cid) { + Report-Error "2. Unknown ClassId: $cid" + $check2Ok = $false + } + + if ($foundClassIds.ContainsKey($cid)) { + Report-Error "2. Duplicate ClassId: $cid" + $check2Ok = $false + } + $foundClassIds[$cid] = $true + + if (-not $objectId -or -not $objectId.InnerText) { + Report-Error "2. ContainedObject missing ObjectId for ClassId $cid" + $check2Ok = $false + } elseif ($objectId.InnerText -notmatch $guidPattern) { + Report-Error "2. Invalid ObjectId '$($objectId.InnerText)' for ClassId $cid" + $check2Ok = $false + } + } + + # Check missing ClassIds + $missingIds = @($validClassIds | Where-Object { -not $foundClassIds.ContainsKey($_) }) + if ($missingIds.Count -gt 0) { + Report-Warn "2. Missing ClassIds: $($missingIds.Count) of 7" + } + + if ($check2Ok) { + Report-OK "2. InternalInfo: $($contained.Count) ContainedObject, all ClassIds valid" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 3: Properties — Name, Synonym, DefaultLanguage, DefaultRunMode --- +if (-not $propsNode) { + Report-Error "3. Properties block missing" +} else { + $check3Ok = $true + + # Name + if (-not $nameNode -or -not $nameNode.InnerText) { + Report-Error "3. Properties: Name is missing or empty" + $check3Ok = $false + } else { + $nameVal = $nameNode.InnerText + if ($nameVal -notmatch $identPattern) { + Report-Error "3. Properties: Name '$nameVal' is not a valid 1C identifier" + $check3Ok = $false + } + } + + # Synonym + $synNode = $propsNode.SelectSingleNode("md:Synonym", $ns) + $synPresent = $false + if ($synNode) { + $synItem = $synNode.SelectSingleNode("v8:item", $ns) + if ($synItem) { + $synContent = $synItem.SelectSingleNode("v8:content", $ns) + if ($synContent -and $synContent.InnerText) { $synPresent = $true } + } + } + + # DefaultLanguage + $defLangNode = $propsNode.SelectSingleNode("md:DefaultLanguage", $ns) + $defLang = if ($defLangNode -and $defLangNode.InnerText) { $defLangNode.InnerText } else { "" } + if (-not $defLang) { + Report-Error "3. Properties: DefaultLanguage is missing or empty" + $check3Ok = $false + } + + # DefaultRunMode + $defRunNode = $propsNode.SelectSingleNode("md:DefaultRunMode", $ns) + if (-not $defRunNode -or -not $defRunNode.InnerText) { + Report-Warn "3. Properties: DefaultRunMode is missing or empty" + } + + if ($check3Ok) { + $synInfo = if ($synPresent) { "Synonym present" } else { "no Synonym" } + Report-OK "3. Properties: Name=`"$objName`", $synInfo, DefaultLanguage=$defLang" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 4: Property values — enum properties --- +if ($propsNode) { + $enumChecked = 0 + $check4Ok = $true + + foreach ($propName in $validEnumValues.Keys) { + $propNode = $propsNode.SelectSingleNode("md:$propName", $ns) + if ($propNode -and $propNode.InnerText) { + $val = $propNode.InnerText + $allowed = $validEnumValues[$propName] + if ($allowed -notcontains $val) { + Report-Error "4. Property '$propName' has invalid value '$val'" + $check4Ok = $false + } + $enumChecked++ + } + } + + if ($check4Ok) { + Report-OK "4. Property values: $enumChecked enum properties checked" + } +} else { + Report-Warn "4. No Properties block to check" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 5: ChildObjects — valid types, no duplicates, order --- +$childObjNode = $cfgNode.SelectSingleNode("md:ChildObjects", $ns) + +if (-not $childObjNode) { + Report-Error "5. ChildObjects block missing" +} else { + $check5Ok = $true + $totalCount = 0 + $typeCounts = @{} + $duplicates = @{} + $typeFirstIndex = @{} # type -> first position index + $lastTypeOrder = -1 + $orderOk = $true + $idx = 0 + + foreach ($child in $childObjNode.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + $typeName = $child.LocalName + $objNameVal = $child.InnerText + + # Valid type? + $typeIdx = $childObjectTypes.IndexOf($typeName) + if ($typeIdx -lt 0) { + Report-Error "5. Unknown type '$typeName' in ChildObjects" + $check5Ok = $false + } else { + # Check order + if (-not $typeFirstIndex.ContainsKey($typeName)) { + $typeFirstIndex[$typeName] = $typeIdx + if ($typeIdx -lt $lastTypeOrder) { + Report-Warn "5. Type '$typeName' is out of canonical order (after type at position $lastTypeOrder)" + $orderOk = $false + } + $lastTypeOrder = $typeIdx + } + } + + # Count and dedup + if (-not $typeCounts.ContainsKey($typeName)) { $typeCounts[$typeName] = @{} } + if ($typeCounts[$typeName].ContainsKey($objNameVal)) { + if (-not $duplicates.ContainsKey("$typeName.$objNameVal")) { + Report-Error "5. Duplicate: $typeName.$objNameVal" + $duplicates["$typeName.$objNameVal"] = $true + $check5Ok = $false + } + } else { + $typeCounts[$typeName][$objNameVal] = $true + } + + $totalCount++ + $idx++ + } + + $typeCount = $typeCounts.Count + if ($check5Ok) { + $orderInfo = if ($orderOk) { ", order correct" } else { "" } + Report-OK "5. ChildObjects: $typeCount types, $totalCount objects${orderInfo}" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 6: DefaultLanguage references existing Language in ChildObjects --- +if ($defLang -and $childObjNode) { + # DefaultLanguage is like "Language.Русский" + $langName = $defLang + if ($langName.StartsWith("Language.")) { + $langName = $langName.Substring(9) + } + + $found = $false + foreach ($child in $childObjNode.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Language" -and $child.InnerText -eq $langName) { + $found = $true; break + } + } + + if ($found) { + Report-OK "6. DefaultLanguage `"$defLang`" found in ChildObjects" + } else { + Report-Error "6. DefaultLanguage `"$defLang`" not found in ChildObjects" + } +} else { + if (-not $defLang) { + Report-Warn "6. Cannot check DefaultLanguage (empty)" + } else { + Report-Warn "6. Cannot check DefaultLanguage (no ChildObjects)" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 7: Language files exist --- +if ($childObjNode) { + $langNames = @() + foreach ($child in $childObjNode.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Language") { + $langNames += $child.InnerText + } + } + + if ($langNames.Count -gt 0) { + $existCount = 0 + foreach ($ln in $langNames) { + $langFile = Join-Path (Join-Path $configDir "Languages") "$ln.xml" + if (Test-Path $langFile) { + $existCount++ + } else { + Report-Warn "7. Language file missing: Languages/$ln.xml" + } + } + if ($existCount -eq $langNames.Count) { + Report-OK "7. Language files: $existCount/$($langNames.Count) exist" + } + } else { + Report-Warn "7. No Language entries in ChildObjects" + } +} else { + Report-Warn "7. Cannot check language files (no ChildObjects)" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 8: Object directories exist (spot-check) --- +if ($childObjNode) { + $dirsToCheck = @{} + foreach ($child in $childObjNode.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + $typeName = $child.LocalName + if ($typeName -eq "Language") { continue } # Already checked + if ($childTypeDirMap.ContainsKey($typeName)) { + $dirName = $childTypeDirMap[$typeName] + if (-not $dirsToCheck.ContainsKey($dirName)) { + $dirsToCheck[$dirName] = 0 + } + $dirsToCheck[$dirName] = $dirsToCheck[$dirName] + 1 + } + } + + $missingDirs = @() + foreach ($dir in $dirsToCheck.Keys) { + $dirPath = Join-Path $configDir $dir + if (-not (Test-Path $dirPath -PathType Container)) { + $missingDirs += "$dir ($($dirsToCheck[$dir]) objects)" + } + } + + if ($missingDirs.Count -eq 0) { + Report-OK "8. Object directories: $($dirsToCheck.Count) directories, all exist" + } else { + foreach ($md in $missingDirs) { + Report-Warn "8. Missing directory: $md" + } + } +} else { + Report-OK "8. Object directories: N/A" +} + +# --- Final output --- +& $finalize + +if ($script:errors -gt 0) { + exit 1 +} +exit 0 diff --git a/.claude/skills/cfe-borrow/SKILL.md b/.claude/skills/cfe-borrow/SKILL.md new file mode 100644 index 00000000..2e6cc164 --- /dev/null +++ b/.claude/skills/cfe-borrow/SKILL.md @@ -0,0 +1,58 @@ +--- +name: cfe-borrow +description: Заимствование объектов из конфигурации 1С в расширение (CFE). Используй когда нужно перехватить метод, изменить форму или добавить реквизит к существующему объекту конфигурации +argument-hint: -ExtensionPath -ConfigPath -Object "Catalog.Контрагенты" +allowed-tools: + - Bash + - Read + - Glob +--- + +# /cfe-borrow — Заимствование объектов из конфигурации + +Заимствует объекты из основной конфигурации в расширение. Создаёт XML-файлы с `ObjectBelonging=Adopted` и `ExtendedConfigurationObject`, добавляет запись в ChildObjects расширения. + +## Предусловие + +Расширение должно быть создано (`/cfe-init`) и содержать валидный `Configuration.xml`. + +## Параметры + +| Параметр | Описание | +|----------|----------| +| `ExtensionPath` | Путь к каталогу расширения (обязат.) | +| `ConfigPath` | Путь к конфигурации-источнику (обязат.) | +| `Object` | Что заимствовать (обязат.), batch через `;;` | + +## Формат -Object + +- `Catalog.Контрагенты` — справочник +- `CommonModule.РаботаСФайлами` — общий модуль +- `Document.РеализацияТоваров` — документ +- `Enum.ВидыОплат` — перечисление +- `Catalog.X ;; CommonModule.Y ;; Enum.Z` — несколько объектов + +Поддерживаются все 44 типа объектов конфигурации. + +## Команда + +```powershell +powershell.exe -NoProfile -File .claude\skills\cfe-borrow\scripts\cfe-borrow.ps1 -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты" +``` + +## Примеры + +```powershell +# Заимствовать один объект +... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты" + +# Несколько объектов за раз +... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты ;; CommonModule.ОбщийМодуль ;; Enum.ВидыОплат" +``` + +## Верификация + +``` +/cfe-validate +``` + diff --git a/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 b/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 new file mode 100644 index 00000000..fcb0a76f --- /dev/null +++ b/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 @@ -0,0 +1,587 @@ +# cfe-borrow v1.0 — Borrow objects from configuration into extension (CFE) +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)][string]$ExtensionPath, + [Parameter(Mandatory)][string]$ConfigPath, + [Parameter(Mandatory)][string]$Object +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +function Info([string]$msg) { Write-Host "[INFO] $msg" } +function Warn([string]$msg) { Write-Host "[WARN] $msg" } + +# --- 1. Resolve paths --- +if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) { + $ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath +} +if (Test-Path $ExtensionPath -PathType Container) { + $candidate = Join-Path $ExtensionPath "Configuration.xml" + if (Test-Path $candidate) { $ExtensionPath = $candidate } + else { Write-Error "No Configuration.xml in extension directory: $ExtensionPath"; exit 1 } +} +if (-not (Test-Path $ExtensionPath)) { Write-Error "Extension file not found: $ExtensionPath"; exit 1 } +$extResolvedPath = (Resolve-Path $ExtensionPath).Path +$extDir = Split-Path $extResolvedPath -Parent + +if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) { + $ConfigPath = Join-Path (Get-Location).Path $ConfigPath +} +if (Test-Path $ConfigPath -PathType Container) { + $candidate = Join-Path $ConfigPath "Configuration.xml" + if (Test-Path $candidate) { $ConfigPath = $candidate } + else { Write-Error "No Configuration.xml in config directory: $ConfigPath"; exit 1 } +} +if (-not (Test-Path $ConfigPath)) { Write-Error "Config file not found: $ConfigPath"; exit 1 } +$cfgResolvedPath = (Resolve-Path $ConfigPath).Path +$cfgDir = Split-Path $cfgResolvedPath -Parent + +# --- 2. Load extension Configuration.xml --- +$script:xmlDoc = New-Object System.Xml.XmlDocument +$script:xmlDoc.PreserveWhitespace = $true +$script:xmlDoc.Load($extResolvedPath) + +$script:mdNs = "http://v8.1c.ru/8.3/MDClasses" +$script:xrNs = "http://v8.1c.ru/8.3/xcf/readable" +$script:xsiNs = "http://www.w3.org/2001/XMLSchema-instance" +$script:v8Ns = "http://v8.1c.ru/8.1/data/core" + +$root = $script:xmlDoc.DocumentElement + +$script:cfgEl = $null +foreach ($child in $root.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Configuration") { + $script:cfgEl = $child; break + } +} +if (-not $script:cfgEl) { Write-Error "No element found in extension"; exit 1 } + +$script:propsEl = $null +$script:childObjsEl = $null +foreach ($child in $script:cfgEl.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + if ($child.LocalName -eq "Properties") { $script:propsEl = $child } + if ($child.LocalName -eq "ChildObjects") { $script:childObjsEl = $child } +} + +if (-not $script:propsEl) { Write-Error "No element found in extension"; exit 1 } +if (-not $script:childObjsEl) { Write-Error "No element found in extension"; exit 1 } + +# --- 3. Extract NamePrefix --- +$script:namePrefix = "" +foreach ($child in $script:propsEl.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "NamePrefix") { + $script:namePrefix = $child.InnerText.Trim(); break + } +} +Info "Extension NamePrefix: $($script:namePrefix)" + +# --- 4. Type mappings --- +$childTypeDirMap = @{ + "Catalog"="Catalogs"; "Document"="Documents"; "Enum"="Enums" + "CommonModule"="CommonModules"; "CommonPicture"="CommonPictures" + "CommonCommand"="CommonCommands"; "CommonTemplate"="CommonTemplates" + "ExchangePlan"="ExchangePlans"; "Report"="Reports"; "DataProcessor"="DataProcessors" + "InformationRegister"="InformationRegisters"; "AccumulationRegister"="AccumulationRegisters" + "ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes" + "ChartOfAccounts"="ChartsOfAccounts"; "AccountingRegister"="AccountingRegisters" + "ChartOfCalculationTypes"="ChartsOfCalculationTypes"; "CalculationRegister"="CalculationRegisters" + "BusinessProcess"="BusinessProcesses"; "Task"="Tasks" + "Subsystem"="Subsystems"; "Role"="Roles"; "Constant"="Constants" + "FunctionalOption"="FunctionalOptions"; "DefinedType"="DefinedTypes" + "FunctionalOptionsParameter"="FunctionalOptionsParameters" + "CommonForm"="CommonForms"; "DocumentJournal"="DocumentJournals" + "SessionParameter"="SessionParameters"; "StyleItem"="StyleItems" + "EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs" + "SettingsStorage"="SettingsStorages"; "FilterCriterion"="FilterCriteria" + "CommandGroup"="CommandGroups"; "DocumentNumerator"="DocumentNumerators" + "Sequence"="Sequences"; "IntegrationService"="IntegrationServices" + "XDTOPackage"="XDTOPackages"; "WebService"="WebServices" + "HTTPService"="HTTPServices"; "WSReference"="WSReferences" + "CommonAttribute"="CommonAttributes"; "Style"="Styles" +} + +# --- 5. Canonical type order (44 types) --- +$script:typeOrder = @( + "Language","Subsystem","StyleItem","Style", + "CommonPicture","SessionParameter","Role","CommonTemplate", + "FilterCriterion","CommonModule","CommonAttribute","ExchangePlan", + "XDTOPackage","WebService","HTTPService","WSReference", + "EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption", + "FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup", + "Constant","CommonForm","Catalog","Document", + "DocumentNumerator","Sequence","DocumentJournal","Enum", + "Report","DataProcessor","InformationRegister","AccumulationRegister", + "ChartOfCharacteristicTypes","ChartOfAccounts","AccountingRegister", + "ChartOfCalculationTypes","CalculationRegister", + "BusinessProcess","Task","IntegrationService" +) + +# --- 6. GeneratedType patterns per type --- +$script:generatedTypes = @{ + "Catalog" = @( + @{ prefix = "CatalogObject"; category = "Object" } + @{ prefix = "CatalogRef"; category = "Ref" } + @{ prefix = "CatalogSelection"; category = "Selection" } + @{ prefix = "CatalogList"; category = "List" } + @{ prefix = "CatalogManager"; category = "Manager" } + ) + "Document" = @( + @{ prefix = "DocumentObject"; category = "Object" } + @{ prefix = "DocumentRef"; category = "Ref" } + @{ prefix = "DocumentSelection"; category = "Selection" } + @{ prefix = "DocumentList"; category = "List" } + @{ prefix = "DocumentManager"; category = "Manager" } + ) + "Enum" = @( + @{ prefix = "EnumRef"; category = "Ref" } + @{ prefix = "EnumManager"; category = "Manager" } + @{ prefix = "EnumList"; category = "List" } + ) + "Constant" = @( + @{ prefix = "ConstantManager"; category = "Manager" } + @{ prefix = "ConstantValueManager"; category = "ValueManager" } + @{ prefix = "ConstantValueKey"; category = "ValueKey" } + ) + "InformationRegister" = @( + @{ prefix = "InformationRegisterRecord"; category = "Record" } + @{ prefix = "InformationRegisterManager"; category = "Manager" } + @{ prefix = "InformationRegisterSelection"; category = "Selection" } + @{ prefix = "InformationRegisterList"; category = "List" } + @{ prefix = "InformationRegisterRecordSet"; category = "RecordSet" } + @{ prefix = "InformationRegisterRecordKey"; category = "RecordKey" } + @{ prefix = "InformationRegisterRecordManager"; category = "RecordManager" } + ) + "AccumulationRegister" = @( + @{ prefix = "AccumulationRegisterRecord"; category = "Record" } + @{ prefix = "AccumulationRegisterManager"; category = "Manager" } + @{ prefix = "AccumulationRegisterSelection"; category = "Selection" } + @{ prefix = "AccumulationRegisterList"; category = "List" } + @{ prefix = "AccumulationRegisterRecordSet"; category = "RecordSet" } + @{ prefix = "AccumulationRegisterRecordKey"; category = "RecordKey" } + ) + "AccountingRegister" = @( + @{ prefix = "AccountingRegisterRecord"; category = "Record" } + @{ prefix = "AccountingRegisterManager"; category = "Manager" } + @{ prefix = "AccountingRegisterSelection"; category = "Selection" } + @{ prefix = "AccountingRegisterList"; category = "List" } + @{ prefix = "AccountingRegisterRecordSet"; category = "RecordSet" } + @{ prefix = "AccountingRegisterRecordKey"; category = "RecordKey" } + ) + "CalculationRegister" = @( + @{ prefix = "CalculationRegisterRecord"; category = "Record" } + @{ prefix = "CalculationRegisterManager"; category = "Manager" } + @{ prefix = "CalculationRegisterSelection"; category = "Selection" } + @{ prefix = "CalculationRegisterList"; category = "List" } + @{ prefix = "CalculationRegisterRecordSet"; category = "RecordSet" } + @{ prefix = "CalculationRegisterRecordKey"; category = "RecordKey" } + ) + "ChartOfAccounts" = @( + @{ prefix = "ChartOfAccountsObject"; category = "Object" } + @{ prefix = "ChartOfAccountsRef"; category = "Ref" } + @{ prefix = "ChartOfAccountsSelection"; category = "Selection" } + @{ prefix = "ChartOfAccountsList"; category = "List" } + @{ prefix = "ChartOfAccountsManager"; category = "Manager" } + ) + "ChartOfCharacteristicTypes" = @( + @{ prefix = "ChartOfCharacteristicTypesObject"; category = "Object" } + @{ prefix = "ChartOfCharacteristicTypesRef"; category = "Ref" } + @{ prefix = "ChartOfCharacteristicTypesSelection"; category = "Selection" } + @{ prefix = "ChartOfCharacteristicTypesList"; category = "List" } + @{ prefix = "ChartOfCharacteristicTypesManager"; category = "Manager" } + ) + "ChartOfCalculationTypes" = @( + @{ prefix = "ChartOfCalculationTypesObject"; category = "Object" } + @{ prefix = "ChartOfCalculationTypesRef"; category = "Ref" } + @{ prefix = "ChartOfCalculationTypesSelection"; category = "Selection" } + @{ prefix = "ChartOfCalculationTypesList"; category = "List" } + @{ prefix = "ChartOfCalculationTypesManager"; category = "Manager" } + @{ prefix = "DisplacingCalculationTypes"; category = "DisplacingCalculationTypes" } + @{ prefix = "BaseCalculationTypes"; category = "BaseCalculationTypes" } + @{ prefix = "LeadingCalculationTypes"; category = "LeadingCalculationTypes" } + ) + "BusinessProcess" = @( + @{ prefix = "BusinessProcessObject"; category = "Object" } + @{ prefix = "BusinessProcessRef"; category = "Ref" } + @{ prefix = "BusinessProcessSelection"; category = "Selection" } + @{ prefix = "BusinessProcessList"; category = "List" } + @{ prefix = "BusinessProcessManager"; category = "Manager" } + ) + "Task" = @( + @{ prefix = "TaskObject"; category = "Object" } + @{ prefix = "TaskRef"; category = "Ref" } + @{ prefix = "TaskSelection"; category = "Selection" } + @{ prefix = "TaskList"; category = "List" } + @{ prefix = "TaskManager"; category = "Manager" } + ) + "ExchangePlan" = @( + @{ prefix = "ExchangePlanObject"; category = "Object" } + @{ prefix = "ExchangePlanRef"; category = "Ref" } + @{ prefix = "ExchangePlanSelection"; category = "Selection" } + @{ prefix = "ExchangePlanList"; category = "List" } + @{ prefix = "ExchangePlanManager"; category = "Manager" } + ) + "DocumentJournal" = @( + @{ prefix = "DocumentJournalSelection"; category = "Selection" } + @{ prefix = "DocumentJournalList"; category = "List" } + @{ prefix = "DocumentJournalManager"; category = "Manager" } + ) + "Report" = @( + @{ prefix = "ReportObject"; category = "Object" } + ) + "DataProcessor" = @( + @{ prefix = "DataProcessorObject"; category = "Object" } + ) +} + +# Types that need ChildObjects element +$typesWithChildObjects = @( + "Catalog","Document","ExchangePlan","ChartOfAccounts", + "ChartOfCharacteristicTypes","ChartOfCalculationTypes", + "BusinessProcess","Task","Enum", + "InformationRegister","AccumulationRegister","AccountingRegister","CalculationRegister" +) + +# CommonModule properties to copy from source +$commonModuleProps = @("Global","ClientManagedApplication","Server","ExternalConnection","ClientOrdinaryApplication","ServerCall") + +# --- 7. XML manipulation helpers (from cf-edit) --- +function Get-ChildIndent($container) { + foreach ($child in $container.ChildNodes) { + if ($child.NodeType -eq 'Whitespace' -or $child.NodeType -eq 'SignificantWhitespace') { + if ($child.Value -match '^\r?\n(\t+)$') { return $Matches[1] } + if ($child.Value -match '^\r?\n(\t+)') { return $Matches[1] } + } + } + $depth = 0; $current = $container + while ($current -and $current -ne $script:xmlDoc.DocumentElement) { $depth++; $current = $current.ParentNode } + return "`t" * ($depth + 1) +} + +function Insert-BeforeElement($container, $newNode, $refNode, $childIndent) { + $ws = $script:xmlDoc.CreateWhitespace("`r`n$childIndent") + if ($refNode) { + $container.InsertBefore($ws, $refNode) | Out-Null + $container.InsertBefore($newNode, $ws) | Out-Null + } else { + $trailing = $container.LastChild + if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) { + $container.InsertBefore($ws, $trailing) | Out-Null + $container.InsertBefore($newNode, $trailing) | Out-Null + } else { + $container.AppendChild($ws) | Out-Null + $container.AppendChild($newNode) | Out-Null + $parentIndent = if ($childIndent.Length -gt 1) { $childIndent.Substring(0, $childIndent.Length - 1) } else { "" } + $closeWs = $script:xmlDoc.CreateWhitespace("`r`n$parentIndent") + $container.AppendChild($closeWs) | Out-Null + } + } +} + +function Expand-SelfClosingElement($container, $parentIndent) { + if (-not $container.HasChildNodes -or $container.IsEmpty) { + $closeWs = $script:xmlDoc.CreateWhitespace("`r`n$parentIndent") + $container.AppendChild($closeWs) | Out-Null + } +} + +# --- 8. Namespaces declaration for object XML --- +$script:xmlnsDecl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"' + +# --- 9. Parse -Object into items --- +$items = @() +foreach ($part in $Object.Split(";;")) { + $trimmed = $part.Trim() + if ($trimmed) { $items += $trimmed } +} + +if ($items.Count -eq 0) { + Write-Error "No objects specified in -Object" + exit 1 +} + +# --- 10. Helper: read source object XML --- +function Read-SourceObject { + param([string]$typeName, [string]$objName) + + $dirName = $childTypeDirMap[$typeName] + if (-not $dirName) { + Write-Error "Unknown type '$typeName'" + exit 1 + } + + $srcFile = Join-Path (Join-Path $cfgDir $dirName) "${objName}.xml" + if (-not (Test-Path $srcFile)) { + Write-Error "Source object not found: $srcFile" + exit 1 + } + + $srcDoc = New-Object System.Xml.XmlDocument + $srcDoc.PreserveWhitespace = $false + $srcDoc.Load($srcFile) + + $srcNs = New-Object System.Xml.XmlNamespaceManager($srcDoc.NameTable) + $srcNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") + $srcNs.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") + + # Find the type element (e.g. ) + $srcRoot = $srcDoc.DocumentElement + $srcEl = $null + foreach ($c in $srcRoot.ChildNodes) { + if ($c.NodeType -eq 'Element') { $srcEl = $c; break } + } + if (-not $srcEl) { + Write-Error "No metadata element found in ${dirName}/${objName}.xml" + exit 1 + } + + # Extract uuid + $srcUuid = $srcEl.GetAttribute("uuid") + if (-not $srcUuid) { + Write-Error "No uuid attribute on source element in ${dirName}/${objName}.xml" + exit 1 + } + + # Extract properties for CommonModule + $srcProps = @{} + $propsNode = $srcEl.SelectSingleNode("md:Properties", $srcNs) + if ($propsNode) { + foreach ($propName in $commonModuleProps) { + $propNode = $propsNode.SelectSingleNode("md:${propName}", $srcNs) + if ($propNode) { + $srcProps[$propName] = $propNode.InnerText.Trim() + } + } + } + + return @{ + Uuid = $srcUuid + Properties = $srcProps + Element = $srcEl + NsManager = $srcNs + } +} + +# --- 11. Helper: generate InternalInfo XML --- +function Build-InternalInfoXml { + param([string]$typeName, [string]$objName, [string]$indent) + + $types = $script:generatedTypes[$typeName] + if (-not $types -or $types.Count -eq 0) { + return "${indent}" + } + + $sb = New-Object System.Text.StringBuilder + $sb.AppendLine("${indent}") | Out-Null + + # ExchangePlan: ThisNode UUID before GeneratedTypes + if ($typeName -eq "ExchangePlan") { + $thisNodeUuid = [guid]::NewGuid().ToString() + $sb.AppendLine("${indent}`t${thisNodeUuid}") | Out-Null + } + + foreach ($gt in $types) { + $fullName = "$($gt.prefix).${objName}" + $typeId = [guid]::NewGuid().ToString() + $valueId = [guid]::NewGuid().ToString() + $sb.AppendLine("${indent}`t") | Out-Null + $sb.AppendLine("${indent}`t`t${typeId}") | Out-Null + $sb.AppendLine("${indent}`t`t${valueId}") | Out-Null + $sb.AppendLine("${indent}`t") | Out-Null + } + + $sb.Append("${indent}") | Out-Null + return $sb.ToString() +} + +# --- 12. Helper: build borrowed object XML --- +function Build-BorrowedObjectXml { + param( + [string]$typeName, + [string]$objName, + [string]$sourceUuid, + [hashtable]$sourceProps + ) + + $newUuid = [guid]::NewGuid().ToString() + $internalInfoXml = Build-InternalInfoXml $typeName $objName "`t`t" + + $sb = New-Object System.Text.StringBuilder + $sb.AppendLine("") | Out-Null + $sb.AppendLine("") | Out-Null + $sb.AppendLine("`t<${typeName} uuid=`"${newUuid}`">") | Out-Null + + # InternalInfo + $sb.AppendLine($internalInfoXml) | Out-Null + + # Properties + $sb.AppendLine("`t`t") | Out-Null + $sb.AppendLine("`t`t`tAdopted") | Out-Null + $sb.AppendLine("`t`t`t${objName}") | Out-Null + $sb.AppendLine("`t`t`t") | Out-Null + $sb.AppendLine("`t`t`t${sourceUuid}") | Out-Null + + # CommonModule: extra properties from source + if ($typeName -eq "CommonModule") { + foreach ($propName in $commonModuleProps) { + $propVal = "false" + if ($sourceProps.ContainsKey($propName)) { + $propVal = $sourceProps[$propName] + } + $sb.AppendLine("`t`t`t<${propName}>${propVal}") | Out-Null + } + } + + $sb.AppendLine("`t`t") | Out-Null + + # ChildObjects (for types that need it) + if ($typesWithChildObjects -contains $typeName) { + $sb.AppendLine("`t`t") | Out-Null + } + + $sb.AppendLine("`t") | Out-Null + $sb.Append("") | Out-Null + + return $sb.ToString() +} + +# --- 13. Helper: add object to extension ChildObjects --- +function Add-ToChildObjects { + param([string]$typeName, [string]$objName) + + $cfgIndent = Get-ChildIndent $script:cfgEl + + # Expand self-closing ChildObjects if needed + if (-not $script:childObjsEl.HasChildNodes -or $script:childObjsEl.IsEmpty) { + Expand-SelfClosingElement $script:childObjsEl $cfgIndent + } + $childIndent = Get-ChildIndent $script:childObjsEl + + $typeIdx = $script:typeOrder.IndexOf($typeName) + if ($typeIdx -lt 0) { + Write-Error "Unknown type '$typeName' for ChildObjects ordering" + exit 1 + } + + # Dedup check + foreach ($child in $script:childObjsEl.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $typeName -and $child.InnerText -eq $objName) { + Warn "Already in ChildObjects: ${typeName}.${objName}" + return + } + } + + # Find insertion point: after last element of same type, or before first element of later type + $insertBefore = $null + $lastSameType = $null + + foreach ($child in $script:childObjsEl.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + $childTypeIdx = $script:typeOrder.IndexOf($child.LocalName) + if ($childTypeIdx -lt 0) { continue } + + if ($child.LocalName -eq $typeName) { + # Same type -- check alphabetical order + if ($child.InnerText -gt $objName -and -not $insertBefore) { + $insertBefore = $child + } + $lastSameType = $child + } elseif ($childTypeIdx -gt $typeIdx -and -not $insertBefore) { + # First element of a later type -- insert before it + $insertBefore = $child + } + } + + # Create element + $newEl = $script:xmlDoc.CreateElement($typeName, $script:mdNs) + $newEl.InnerText = $objName + + if ($insertBefore) { + Insert-BeforeElement $script:childObjsEl $newEl $insertBefore $childIndent + } else { + Insert-BeforeElement $script:childObjsEl $newEl $null $childIndent + } + + Info "Added to ChildObjects: ${typeName}.${objName}" +} + +# --- 14. Process each item --- +$borrowedFiles = @() +$borrowedCount = 0 + +foreach ($item in $items) { + $dotIdx = $item.IndexOf(".") + if ($dotIdx -lt 1) { + Write-Error "Invalid format '${item}', expected 'Type.Name'" + exit 1 + } + $typeName = $item.Substring(0, $dotIdx) + $objName = $item.Substring($dotIdx + 1) + + if (-not $childTypeDirMap.ContainsKey($typeName)) { + Write-Error "Unknown type '${typeName}'" + exit 1 + } + + $dirName = $childTypeDirMap[$typeName] + + Info "Borrowing ${typeName}.${objName}..." + + # Read source object + $src = Read-SourceObject $typeName $objName + Info " Source UUID: $($src.Uuid)" + + # Build borrowed object XML + $borrowedXml = Build-BorrowedObjectXml $typeName $objName $src.Uuid $src.Properties + + # Create directory in extension if needed + $targetDir = Join-Path $extDir $dirName + if (-not (Test-Path $targetDir)) { + New-Item -ItemType Directory -Path $targetDir -Force | Out-Null + } + + # Write borrowed object XML with UTF-8 BOM + $targetFile = Join-Path $targetDir "${objName}.xml" + $enc = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllText($targetFile, $borrowedXml, $enc) + Info " Created: $targetFile" + + # Add to ChildObjects + Add-ToChildObjects $typeName $objName + + $borrowedFiles += $targetFile + $borrowedCount++ +} + +# --- 15. Save modified Configuration.xml --- +$settings = New-Object System.Xml.XmlWriterSettings +$settings.Encoding = New-Object System.Text.UTF8Encoding($true) +$settings.Indent = $false +$settings.NewLineHandling = [System.Xml.NewLineHandling]::None + +$memStream = New-Object System.IO.MemoryStream +$writer = [System.Xml.XmlWriter]::Create($memStream, $settings) +$script:xmlDoc.Save($writer) +$writer.Flush(); $writer.Close() + +$bytes = $memStream.ToArray() +$memStream.Close() +$text = [System.Text.Encoding]::UTF8.GetString($bytes) +if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) } +$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"') + +$utf8Bom = New-Object System.Text.UTF8Encoding($true) +[System.IO.File]::WriteAllText($extResolvedPath, $text, $utf8Bom) +Info "Saved: $extResolvedPath" + +# --- 16. Summary --- +Write-Host "" +Write-Host "=== cfe-borrow summary ===" +Write-Host " Extension: $extDir" +Write-Host " Config: $cfgDir" +Write-Host " Borrowed: $borrowedCount object(s)" +foreach ($f in $borrowedFiles) { + Write-Host " - $f" +} +exit 0 diff --git a/.claude/skills/cfe-diff/SKILL.md b/.claude/skills/cfe-diff/SKILL.md new file mode 100644 index 00000000..5214937a --- /dev/null +++ b/.claude/skills/cfe-diff/SKILL.md @@ -0,0 +1,61 @@ +--- +name: cfe-diff +description: Анализ расширения конфигурации 1С (CFE) — обзор изменений и проверка переноса. Используй для понимания что изменено в расширении или для проверки перенесены ли изменения из расширения в конфигурацию +argument-hint: -ExtensionPath -ConfigPath [-Mode A|B] +allowed-tools: + - Bash + - Read + - Glob +--- + +# /cfe-diff — Анализ расширения конфигурации + +Анализирует расширение в двух режимах: обзор изменений (Mode A) или проверка переноса (Mode B). + +## Параметры + +| Параметр | Описание | По умолчанию | +|----------|----------|--------------| +| `ExtensionPath` | Путь к расширению (обязат.) | — | +| `ConfigPath` | Путь к конфигурации (обязат.) | — | +| `Mode` | `A` (обзор) / `B` (проверка переноса) | `A` | + +## Команда + +```powershell +powershell.exe -NoProfile -File .claude\skills\cfe-diff\scripts\cfe-diff.ps1 -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A +``` + +## Mode A — обзор расширения + +Для каждого объекта показывает: +- `[BORROWED]` — заимствованный: перехватчики (`&Перед`, `&После`, `&ИзменениеИКонтроль`, `&Вместо`), собственные реквизиты/ТЧ/формы +- `[OWN]` — собственный: количество реквизитов, ТЧ, форм + +Пример вывода: +``` +[BORROWED] Catalog.Валюты + &ИзменениеИКонтроль("РеквизитыРедактируемыеВГрупповойОбработке") — line 4 in ... + &Перед("ЗагрузитьКурсыВалют") — line 13 in ... + ChildObjects: 1 own attrs, 1 own TS, 3 own forms +[OWN] Catalog.Расш5_Справочник1 +``` + +## Mode B — проверка переноса + +Для каждого `&ИзменениеИКонтроль` извлекает блоки `#Вставка`/`#КонецВставки` из расширения и ищет их в соответствующем модуле конфигурации. + +Статусы: +- `[TRANSFERRED]` — код найден в конфигурации +- `[NOT_TRANSFERRED]` — код не найден +- `[NEEDS_REVIEW]` — нет блоков `#Вставка` или модуль конфигурации не найден + +## Примеры + +```powershell +# Обзор — что изменено в расширении +... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A + +# Проверка переноса — все ли #Вставка перенесены +... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode B +``` diff --git a/.claude/skills/cfe-diff/scripts/cfe-diff.ps1 b/.claude/skills/cfe-diff/scripts/cfe-diff.ps1 new file mode 100644 index 00000000..4177d184 --- /dev/null +++ b/.claude/skills/cfe-diff/scripts/cfe-diff.ps1 @@ -0,0 +1,392 @@ +# cfe-diff v1.0 — Analyze and compare 1C configuration extension (CFE) +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)] + [string]$ExtensionPath, + + [Parameter(Mandatory)] + [string]$ConfigPath, + + [ValidateSet("A","B")] + [string]$Mode = "A" +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve paths --- +if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) { + $ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath +} +if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) { + $ConfigPath = Join-Path (Get-Location).Path $ConfigPath +} +if (Test-Path $ExtensionPath -PathType Leaf) { $ExtensionPath = Split-Path $ExtensionPath -Parent } +if (Test-Path $ConfigPath -PathType Leaf) { $ConfigPath = Split-Path $ConfigPath -Parent } + +$extCfg = Join-Path $ExtensionPath "Configuration.xml" +$srcCfg = Join-Path $ConfigPath "Configuration.xml" +if (-not (Test-Path $extCfg)) { Write-Error "Extension Configuration.xml not found: $extCfg"; exit 1 } +if (-not (Test-Path $srcCfg)) { Write-Error "Config Configuration.xml not found: $srcCfg"; exit 1 } + +# --- Type -> directory mapping --- +$childTypeDirMap = @{ + "Catalog"="Catalogs"; "Document"="Documents"; "Enum"="Enums" + "CommonModule"="CommonModules"; "CommonPicture"="CommonPictures" + "CommonCommand"="CommonCommands"; "CommonTemplate"="CommonTemplates" + "ExchangePlan"="ExchangePlans"; "Report"="Reports"; "DataProcessor"="DataProcessors" + "InformationRegister"="InformationRegisters"; "AccumulationRegister"="AccumulationRegisters" + "ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes" + "ChartOfAccounts"="ChartsOfAccounts"; "AccountingRegister"="AccountingRegisters" + "ChartOfCalculationTypes"="ChartsOfCalculationTypes"; "CalculationRegister"="CalculationRegisters" + "BusinessProcess"="BusinessProcesses"; "Task"="Tasks" + "Subsystem"="Subsystems"; "Role"="Roles"; "Constant"="Constants" + "FunctionalOption"="FunctionalOptions"; "DefinedType"="DefinedTypes" + "FunctionalOptionsParameter"="FunctionalOptionsParameters" + "CommonForm"="CommonForms"; "DocumentJournal"="DocumentJournals" + "SessionParameter"="SessionParameters"; "StyleItem"="StyleItems" + "EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs" + "SettingsStorage"="SettingsStorages"; "FilterCriterion"="FilterCriteria" + "CommandGroup"="CommandGroups"; "DocumentNumerator"="DocumentNumerators" + "Sequence"="Sequences"; "IntegrationService"="IntegrationServices" + "CommonAttribute"="CommonAttributes" +} + +# --- Parse extension Configuration.xml --- +$extDoc = New-Object System.Xml.XmlDocument +$extDoc.PreserveWhitespace = $false +$extDoc.Load($extCfg) + +$ns = New-Object System.Xml.XmlNamespaceManager($extDoc.NameTable) +$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") +$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") + +$extProps = $extDoc.SelectSingleNode("//md:Configuration/md:Properties", $ns) +$extNameNode = $extProps.SelectSingleNode("md:Name", $ns) +$extName = if ($extNameNode) { $extNameNode.InnerText } else { "?" } +$prefixNode = $extProps.SelectSingleNode("md:NamePrefix", $ns) +$namePrefix = if ($prefixNode -and $prefixNode.InnerText) { $prefixNode.InnerText } else { "" } +$purposeNode = $extProps.SelectSingleNode("md:ConfigurationExtensionPurpose", $ns) +$purpose = if ($purposeNode) { $purposeNode.InnerText } else { "?" } + +Write-Host "=== cfe-diff Mode ${Mode}: $extName (${purpose}) ===" +Write-Host " NamePrefix: $namePrefix" +Write-Host "" + +# --- Collect ChildObjects --- +$childObjNode = $extDoc.SelectSingleNode("//md:Configuration/md:ChildObjects", $ns) +if (-not $childObjNode) { + Write-Host "[WARN] No ChildObjects in extension" + exit 0 +} + +$objects = @() +foreach ($child in $childObjNode.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + if ($child.LocalName -eq "Language") { continue } + $objects += @{ Type = $child.LocalName; Name = $child.InnerText } +} + +if ($objects.Count -eq 0) { + Write-Host "No objects (besides Language) in extension." + exit 0 +} + +# --- Helper: check if object is borrowed --- +function Get-ObjectInfo { + param([string]$objType, [string]$objName) + + if (-not $childTypeDirMap.ContainsKey($objType)) { return $null } + $dirName = $childTypeDirMap[$objType] + $objFile = Join-Path (Join-Path $ExtensionPath $dirName) "${objName}.xml" + + if (-not (Test-Path $objFile)) { return @{ Borrowed = $false; File = $objFile; Exists = $false } } + + $doc = New-Object System.Xml.XmlDocument + $doc.PreserveWhitespace = $false + $doc.Load($objFile) + + $objNs = New-Object System.Xml.XmlNamespaceManager($doc.NameTable) + $objNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") + + $objEl = $null + foreach ($c in $doc.DocumentElement.ChildNodes) { + if ($c.NodeType -eq 'Element') { $objEl = $c; break } + } + if (-not $objEl) { return @{ Borrowed = $false; File = $objFile; Exists = $true } } + + $propsEl = $objEl.SelectSingleNode("md:Properties", $objNs) + $obNode = if ($propsEl) { $propsEl.SelectSingleNode("md:ObjectBelonging", $objNs) } else { $null } + + $info = @{ + Borrowed = ($obNode -and $obNode.InnerText -eq "Adopted") + File = $objFile + Exists = $true + Type = $objType + Name = $objName + DirName = $dirName + ObjElement = $objEl + ObjNs = $objNs + } + return $info +} + +# --- Helper: find .bsl files for object --- +function Get-BslFiles { + param([string]$objType, [string]$objName) + + if (-not $childTypeDirMap.ContainsKey($objType)) { return @() } + $dirName = $childTypeDirMap[$objType] + $objDir = Join-Path (Join-Path $ExtensionPath $dirName) $objName + + if (-not (Test-Path $objDir -PathType Container)) { return @() } + + $bslFiles = @() + $extDir = Join-Path $objDir "Ext" + if (Test-Path $extDir) { + $items = Get-ChildItem -Path $extDir -Filter "*.bsl" -ErrorAction SilentlyContinue + foreach ($item in $items) { $bslFiles += $item.FullName } + } + + # Forms + $formsDir = Join-Path $objDir "Forms" + if (Test-Path $formsDir) { + $formModules = Get-ChildItem -Path $formsDir -Recurse -Filter "Module.bsl" -ErrorAction SilentlyContinue + foreach ($fm in $formModules) { $bslFiles += $fm.FullName } + } + + return $bslFiles +} + +# --- Helper: parse interceptors from .bsl --- +function Get-Interceptors { + param([string]$bslPath) + + if (-not (Test-Path $bslPath)) { return @() } + $lines = [System.IO.File]::ReadAllLines($bslPath, [System.Text.Encoding]::UTF8) + $interceptors = @() + $i = 0 + while ($i -lt $lines.Count) { + $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 $interceptors +} + +# --- Helper: extract #Вставка blocks from .bsl --- +function Get-InsertionBlocks { + param([string]$bslPath) + + if (-not (Test-Path $bslPath)) { return @() } + $lines = [System.IO.File]::ReadAllLines($bslPath, [System.Text.Encoding]::UTF8) + $blocks = @() + $inBlock = $false + $blockLines = @() + $startLine = 0 + + for ($i = 0; $i -lt $lines.Count; $i++) { + $line = $lines[$i].Trim() + if ($line -eq "#Вставка") { + $inBlock = $true + $blockLines = @() + $startLine = $i + 1 + } elseif ($line -eq "#КонецВставки" -and $inBlock) { + $inBlock = $false + $blocks += @{ + StartLine = $startLine + EndLine = $i + 1 + Code = ($blockLines -join "`n").Trim() + File = $bslPath + } + } elseif ($inBlock) { + $blockLines += $lines[$i] + } + } + return $blocks +} + +# ============================================================ +# MODE A: Extension overview +# ============================================================ +if ($Mode -eq "A") { + $borrowedList = @() + $ownList = @() + + foreach ($obj in $objects) { + $info = Get-ObjectInfo $obj.Type $obj.Name + if (-not $info) { + Write-Host " [?] $($obj.Type).$($obj.Name) — unknown type" + continue + } + if (-not $info.Exists) { + Write-Host " [?] $($obj.Type).$($obj.Name) — file not found" + continue + } + + if ($info.Borrowed) { + $borrowedList += $obj + + Write-Host " [BORROWED] $($obj.Type).$($obj.Name)" + + # Find .bsl files and interceptors + $bslFiles = Get-BslFiles $obj.Type $obj.Name + foreach ($bsl in $bslFiles) { + $relPath = $bsl.Replace($ExtensionPath, "").TrimStart("\", "/") + $interceptors = Get-Interceptors $bsl + if ($interceptors.Count -gt 0) { + foreach ($ic in $interceptors) { + Write-Host " &$($ic.Type)(`"$($ic.Method)`") — line $($ic.Line) in $relPath" + } + } else { + Write-Host " $relPath (no interceptors)" + } + } + + # Check for own attributes/forms in ChildObjects + if ($info.ObjElement) { + $childObj = $info.ObjElement.SelectSingleNode("md:ChildObjects", $info.ObjNs) + if ($childObj) { + $ownAttrs = 0 + $ownForms = 0 + $ownTS = 0 + $borrowedItems = 0 + foreach ($c in $childObj.ChildNodes) { + if ($c.NodeType -ne 'Element') { continue } + $cProps = $c.SelectSingleNode("md:Properties", $info.ObjNs) + if ($cProps) { + $cOb = $cProps.SelectSingleNode("md:ObjectBelonging", $info.ObjNs) + if ($cOb -and $cOb.InnerText -eq "Adopted") { + $borrowedItems++ + continue + } + } + switch ($c.LocalName) { + "Attribute" { $ownAttrs++ } + "TabularSection" { $ownTS++ } + "Form" { $ownForms++ } + } + } + $parts = @() + if ($ownAttrs -gt 0) { $parts += "$ownAttrs own attrs" } + if ($ownTS -gt 0) { $parts += "$ownTS own TS" } + if ($ownForms -gt 0) { $parts += "$ownForms own forms" } + if ($borrowedItems -gt 0) { $parts += "$borrowedItems borrowed items" } + if ($parts.Count -gt 0) { + Write-Host " ChildObjects: $($parts -join ', ')" + } + } + } + } else { + $ownList += $obj + Write-Host " [OWN] $($obj.Type).$($obj.Name)" + + # Brief info for own objects + if ($info.ObjElement) { + $childObj = $info.ObjElement.SelectSingleNode("md:ChildObjects", $info.ObjNs) + if ($childObj) { + $attrs = 0; $forms = 0; $ts = 0 + foreach ($c in $childObj.ChildNodes) { + if ($c.NodeType -ne 'Element') { continue } + switch ($c.LocalName) { + "Attribute" { $attrs++ } + "TabularSection" { $ts++ } + "Form" { $forms++ } + } + } + $parts = @() + if ($attrs -gt 0) { $parts += "$attrs attrs" } + if ($ts -gt 0) { $parts += "$ts TS" } + if ($forms -gt 0) { $parts += "$forms forms" } + if ($parts.Count -gt 0) { + Write-Host " $($parts -join ', ')" + } + } + } + } + } + + Write-Host "" + Write-Host "=== Summary: $($borrowedList.Count) borrowed, $($ownList.Count) own objects ===" +} + +# ============================================================ +# MODE B: Transfer check +# ============================================================ +if ($Mode -eq "B") { + $transferred = 0 + $notTransferred = 0 + $needsReview = 0 + + foreach ($obj in $objects) { + $info = Get-ObjectInfo $obj.Type $obj.Name + if (-not $info -or -not $info.Exists -or -not $info.Borrowed) { continue } + + # Find .bsl files with &ИзменениеИКонтроль + $bslFiles = Get-BslFiles $obj.Type $obj.Name + foreach ($bsl in $bslFiles) { + $interceptors = Get-Interceptors $bsl + $macInterceptors = @($interceptors | Where-Object { $_.Type -eq "ИзменениеИКонтроль" }) + + if ($macInterceptors.Count -eq 0) { continue } + + foreach ($ic in $macInterceptors) { + $methodName = $ic.Method + $relBsl = $bsl.Replace($ExtensionPath, "").TrimStart("\", "/") + + # Find #Вставка blocks in this file + $insertBlocks = Get-InsertionBlocks $bsl + + if ($insertBlocks.Count -eq 0) { + Write-Host " [NEEDS_REVIEW] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — no #Вставка blocks" + $needsReview++ + continue + } + + # Find corresponding module in config + if (-not $childTypeDirMap.ContainsKey($obj.Type)) { continue } + $dirName = $childTypeDirMap[$obj.Type] + $configBsl = $bsl.Replace($ExtensionPath, $ConfigPath) + + if (-not (Test-Path $configBsl)) { + Write-Host " [NEEDS_REVIEW] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — config module not found" + $needsReview++ + continue + } + + $configContent = [System.IO.File]::ReadAllText($configBsl, [System.Text.Encoding]::UTF8) + + $allTransferred = $true + foreach ($block in $insertBlocks) { + $code = $block.Code + if (-not $code) { continue } + + # Normalize whitespace for comparison + $codeNorm = $code -replace '\s+', ' ' + $configNorm = $configContent -replace '\s+', ' ' + + if ($configNorm.Contains($codeNorm)) { + # Found in config + } else { + $allTransferred = $false + } + } + + if ($allTransferred) { + Write-Host " [TRANSFERRED] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — $($insertBlocks.Count) block(s)" + $transferred++ + } else { + Write-Host " [NOT_TRANSFERRED] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — some blocks not found in config" + $notTransferred++ + } + } + } + } + + Write-Host "" + Write-Host "=== Transfer check: $transferred transferred, $notTransferred not transferred, $needsReview needs review ===" +} diff --git a/.claude/skills/cfe-init/SKILL.md b/.claude/skills/cfe-init/SKILL.md new file mode 100644 index 00000000..295330a9 --- /dev/null +++ b/.claude/skills/cfe-init/SKILL.md @@ -0,0 +1,74 @@ +--- +name: cfe-init +description: Создать расширение конфигурации 1С (CFE) — scaffold XML-исходников. Используй когда нужно создать новое расширение для исправления, доработки или дополнения конфигурации +argument-hint: [-Purpose Patch|Customization|AddOn] [-CompatibilityMode Version8_3_24] +allowed-tools: + - Bash + - Read + - Glob +--- + +# /cfe-init — Создание расширения конфигурации 1С + +Создаёт scaffold расширения: `Configuration.xml`, `Languages/Русский.xml`, опционально `Roles/`. + +## Подготовка + +Перед созданием расширения рекомендуется получить версию и режим совместимости базовой конфигурации: + +``` +/cf-info -Mode brief +``` + +Это даст `CompatibilityMode` (передать в `-CompatibilityMode`) и версию конфигурации (для `-Version`, например `<ВерсияКонфигурации>.1`). + +## Параметры + +| Параметр | Описание | По умолчанию | +|----------|----------|--------------| +| `Name` | Имя расширения (обязат.) | — | +| `Synonym` | Синоним | = Name | +| `NamePrefix` | Префикс собственных объектов | = Name + "_" | +| `OutputDir` | Каталог для создания | `src` | +| `Purpose` | `Patch` (исправление) / `Customization` (доработка) / `AddOn` (дополнение) | `Customization` | +| `Version` | Версия расширения | — | +| `Vendor` | Поставщик | — | +| `CompatibilityMode` | Режим совместимости | `Version8_3_24` | +| `NoRole` | Без основной роли | false | + +## Команда + +```powershell +powershell.exe -NoProfile -File .claude\skills\cfe-init\scripts\cfe-init.ps1 -Name "МоёРасширение" +``` + +## Что создаётся + +``` +/ +├── Configuration.xml # Свойства расширения +├── Languages/ +│ └── Русский.xml # Язык (заимствованный) +└── Roles/ # Если не -NoRole + └── ОсновнаяРоль.xml +``` + +## Примеры + +```powershell +# Расширение-исправление для ERP +... -Name Расш1 -Purpose Patch -CompatibilityMode Version8_3_17 -OutputDir src + +# Расширение-доработка с версией +... -Name МоёРасширение -Version "1.0.0.1" -Vendor "Компания" -OutputDir src + +# Без роли, с явным префиксом +... -Name ИсправлениеБага -NamePrefix "ИБ_" -Purpose Patch -NoRole -OutputDir src +``` + +## Верификация + +``` +/cfe-validate +``` + diff --git a/.claude/skills/cfe-init/scripts/cfe-init.ps1 b/.claude/skills/cfe-init/scripts/cfe-init.ps1 new file mode 100644 index 00000000..b8f449ad --- /dev/null +++ b/.claude/skills/cfe-init/scripts/cfe-init.ps1 @@ -0,0 +1,208 @@ +# cfe-init v1.0 — Create 1C configuration extension scaffold (CFE) +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)] + [string]$Name, + [string]$Synonym = $Name, + [string]$NamePrefix, + [string]$OutputDir = "src", + [ValidateSet("Patch","Customization","AddOn")] + [string]$Purpose = "Customization", + [string]$Version, + [string]$Vendor, + [string]$CompatibilityMode = "Version8_3_24", + [switch]$NoRole +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Default NamePrefix --- +if (-not $NamePrefix) { + $NamePrefix = "${Name}_" +} + +# --- Resolve output dir --- +if (-not [System.IO.Path]::IsPathRooted($OutputDir)) { + $OutputDir = Join-Path (Get-Location).Path $OutputDir +} + +# --- Check existing --- +$cfgFile = Join-Path $OutputDir "Configuration.xml" +if (Test-Path $cfgFile) { + Write-Error "Configuration.xml already exists: $cfgFile" + exit 1 +} + +# --- Generate UUIDs --- +$uuidCfg = [guid]::NewGuid().ToString() +$uuidLang = [guid]::NewGuid().ToString() +$uuidRole = [guid]::NewGuid().ToString() + +# 7 ContainedObject ObjectIds +$co1 = [guid]::NewGuid().ToString() +$co2 = [guid]::NewGuid().ToString() +$co3 = [guid]::NewGuid().ToString() +$co4 = [guid]::NewGuid().ToString() +$co5 = [guid]::NewGuid().ToString() +$co6 = [guid]::NewGuid().ToString() +$co7 = [guid]::NewGuid().ToString() + +# --- Synonym XML --- +$synonymXml = "" +if ($Synonym) { + $synonymXml = "`r`n`t`t`t`t`r`n`t`t`t`t`tru`r`n`t`t`t`t`t$([System.Security.SecurityElement]::Escape($Synonym))`r`n`t`t`t`t`r`n`t`t`t" +} + +# --- Optional properties --- +$vendorXml = if ($Vendor) { [System.Security.SecurityElement]::Escape($Vendor) } else { "" } +$versionXml = if ($Version) { [System.Security.SecurityElement]::Escape($Version) } else { "" } + +# --- Role name --- +$roleName = "${NamePrefix}ОсновнаяРоль" + +# --- DefaultRoles XML --- +$defaultRolesXml = "" +if (-not $NoRole) { + $defaultRolesXml = "`r`n`t`t`t`tRole.$roleName`r`n`t`t`t" +} + +# --- ChildObjects --- +$childObjectsXml = "`r`n`t`t`tРусский" +if (-not $NoRole) { + $childObjectsXml += "`r`n`t`t`t$roleName" +} +$childObjectsXml += "`r`n`t`t" + +# --- Configuration.xml --- +$cfgXml = @" + + + + + + 9cd510cd-abfc-11d4-9434-004095e12fc7 + $co1 + + + 9fcd25a0-4822-11d4-9414-008048da11f9 + $co2 + + + e3687481-0a87-462c-a166-9f34594f9bba + $co3 + + + 9de14907-ec23-4a07-96f0-85521cb6b53b + $co4 + + + 51f2d5d8-ea4d-4064-8892-82951750031e + $co5 + + + e68182ea-4237-4383-967f-90c1e3370bc7 + $co6 + + + fb282519-d103-4dd3-bc12-cb271d631dfc + $co7 + + + + Adopted + $([System.Security.SecurityElement]::Escape($Name)) + $synonymXml + + $Purpose + true + $([System.Security.SecurityElement]::Escape($NamePrefix)) + $CompatibilityMode + ManagedApplication + + PlatformApplication + + Russian + $defaultRolesXml + $vendorXml + $versionXml + Language.Русский + + + + + + TaxiEnableVersion8_2 + + $childObjectsXml + + +"@ + +# --- Languages/Русский.xml (adopted format) --- +$langXml = @" + + + + + + Adopted + Русский + + 00000000-0000-0000-0000-000000000000 + ru + + + +"@ + +# --- Role XML --- +$roleXml = @" + + + + + $([System.Security.SecurityElement]::Escape($roleName)) + + + + + +"@ + +# --- Create directories --- +if (-not (Test-Path $OutputDir)) { + New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null +} +$langDir = Join-Path $OutputDir "Languages" +if (-not (Test-Path $langDir)) { + New-Item -ItemType Directory -Path $langDir -Force | Out-Null +} + +# --- Write files with UTF-8 BOM --- +$enc = New-Object System.Text.UTF8Encoding($true) + +[System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc) +$langFile = Join-Path $langDir "Русский.xml" +[System.IO.File]::WriteAllText($langFile, $langXml, $enc) + +# --- Role --- +if (-not $NoRole) { + $roleDir = Join-Path $OutputDir "Roles" + if (-not (Test-Path $roleDir)) { + New-Item -ItemType Directory -Path $roleDir -Force | Out-Null + } + $roleFile = Join-Path $roleDir "$roleName.xml" + [System.IO.File]::WriteAllText($roleFile, $roleXml, $enc) +} + +# --- Output --- +Write-Host "[OK] Создано расширение: $Name" +Write-Host " Каталог: $OutputDir" +Write-Host " Назначение: $Purpose" +Write-Host " Префикс: $NamePrefix" +Write-Host " Configuration.xml: $cfgFile" +Write-Host " Languages: $langFile" +if (-not $NoRole) { + Write-Host " Role: $roleFile" +} diff --git a/.claude/skills/cfe-patch-method/SKILL.md b/.claude/skills/cfe-patch-method/SKILL.md new file mode 100644 index 00000000..b51740f2 --- /dev/null +++ b/.claude/skills/cfe-patch-method/SKILL.md @@ -0,0 +1,78 @@ +--- +name: cfe-patch-method +description: Генерация перехватчика метода в расширении 1С (CFE). Используй когда нужно перехватить метод заимствованного объекта — вставить код до, после или вместо оригинального +argument-hint: -ExtensionPath -ModulePath "Catalog.X.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before +allowed-tools: + - Bash + - Read + - Glob +--- + +# /cfe-patch-method — Генерация перехватчика метода + +Генерирует `.bsl` файл с декоратором перехвата для заимствованного объекта расширения. Создаёт файл или дописывает в существующий. + +## Предусловие + +Объект должен быть заимствован в расширение (`/cfe-borrow`). Скрипт читает `NamePrefix` из `Configuration.xml` расширения для формирования имени процедуры. + +## Параметры + +| Параметр | Описание | По умолчанию | +|----------|----------|--------------| +| `ExtensionPath` | Путь к расширению (обязат.) | — | +| `ModulePath` | Путь к модулю (обязат.) | — | +| `MethodName` | Имя перехватываемого метода (обязат.) | — | +| `InterceptorType` | `Before` / `After` / `ModificationAndControl` (обязат.) | — | +| `Context` | Директива контекста | `НаСервере` | +| `IsFunction` | Метод — функция (добавит `Возврат`) | false | + +## Формат ModulePath + +| ModulePath | Файл | +|------------|------| +| `Catalog.X.ObjectModule` | `Catalogs/X/Ext/ObjectModule.bsl` | +| `Catalog.X.ManagerModule` | `Catalogs/X/Ext/ManagerModule.bsl` | +| `Catalog.X.Form.Y` | `Catalogs/X/Forms/Y/Ext/Form/Module.bsl` | +| `CommonModule.X` | `CommonModules/X/Ext/Module.bsl` | +| `Document.X.ObjectModule` | `Documents/X/Ext/ObjectModule.bsl` | +| `Document.X.Form.Y` | `Documents/X/Forms/Y/Ext/Form/Module.bsl` | + +Аналогично для Report, DataProcessor, InformationRegister и других типов. + +## Типы перехвата + +| InterceptorType | Декоратор | Назначение | +|-----------------|-----------|------------| +| `Before` | `&Перед` | Код до вызова оригинального метода | +| `After` | `&После` | Код после вызова оригинального метода | +| `ModificationAndControl` | `&ИзменениеИКонтроль` | Копия тела метода с маркерами `#Вставка`/`#Удаление` | + +## Команда + +```powershell +powershell.exe -NoProfile -File .claude\skills\cfe-patch-method\scripts\cfe-patch-method.ps1 -ExtensionPath src -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before +``` + +## Примеры + +```powershell +# Перехват &Перед на сервере +... -ExtensionPath src -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before + +# Перехват &После на клиенте +... -ExtensionPath src -ModulePath "Document.Заказ.Form.ФормаДокумента" -MethodName "ПослеЗаписиНаСервере" -InterceptorType After -Context "НаКлиенте" + +# ИзменениеИКонтроль для функции +... -ExtensionPath src -ModulePath "CommonModule.ОбщийМодуль" -MethodName "ПолучитьДанные" -InterceptorType ModificationAndControl -IsFunction +``` + +## Генерируемый код (Before) + +```bsl +&НаСервере +&Перед("ПриЗаписи") +Процедура Расш1_ПриЗаписи() + // TODO: код перед вызовом оригинального метода +КонецПроцедуры +``` diff --git a/.claude/skills/cfe-patch-method/scripts/cfe-patch-method.ps1 b/.claude/skills/cfe-patch-method/scripts/cfe-patch-method.ps1 new file mode 100644 index 00000000..7c3bf2d0 --- /dev/null +++ b/.claude/skills/cfe-patch-method/scripts/cfe-patch-method.ps1 @@ -0,0 +1,186 @@ +# cfe-patch-method v1.0 — Generate method interceptor for 1C extension (CFE) +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)] + [string]$ExtensionPath, + + [Parameter(Mandatory)] + [string]$ModulePath, + + [Parameter(Mandatory)] + [string]$MethodName, + + [Parameter(Mandatory)] + [ValidateSet("Before","After","ModificationAndControl")] + [string]$InterceptorType, + + [string]$Context = "НаСервере", + + [switch]$IsFunction +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve extension path --- +if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) { + $ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath +} +if (Test-Path $ExtensionPath -PathType Leaf) { + $ExtensionPath = Split-Path $ExtensionPath -Parent +} +$cfgFile = Join-Path $ExtensionPath "Configuration.xml" +if (-not (Test-Path $cfgFile)) { + Write-Error "Configuration.xml not found in: $ExtensionPath" + exit 1 +} + +# --- Read NamePrefix from Configuration.xml --- +$cfgDoc = New-Object System.Xml.XmlDocument +$cfgDoc.PreserveWhitespace = $false +$cfgDoc.Load($cfgFile) + +$cfgNs = New-Object System.Xml.XmlNamespaceManager($cfgDoc.NameTable) +$cfgNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") + +$propsNode = $cfgDoc.SelectSingleNode("//md:Configuration/md:Properties", $cfgNs) +$prefixNode = if ($propsNode) { $propsNode.SelectSingleNode("md:NamePrefix", $cfgNs) } else { $null } +$namePrefix = if ($prefixNode -and $prefixNode.InnerText) { $prefixNode.InnerText } else { "Расш_" } + +# --- Map ModulePath to file path --- +# ModulePath formats: +# Catalog.X.ObjectModule -> Catalogs/X/Ext/ObjectModule.bsl +# Catalog.X.ManagerModule -> Catalogs/X/Ext/ManagerModule.bsl +# Catalog.X.Form.Y -> Catalogs/X/Forms/Y/Ext/Form/Module.bsl +# CommonModule.X -> CommonModules/X/Ext/Module.bsl +# Document.X.ObjectModule -> Documents/X/Ext/ObjectModule.bsl +# Document.X.ManagerModule -> Documents/X/Ext/ManagerModule.bsl +# Document.X.Form.Y -> Documents/X/Forms/Y/Ext/Form/Module.bsl + +$typeDirMap = @{ + "Catalog"="Catalogs"; "Document"="Documents"; "Enum"="Enums" + "CommonModule"="CommonModules"; "Report"="Reports"; "DataProcessor"="DataProcessors" + "ExchangePlan"="ExchangePlans"; "ChartOfAccounts"="ChartsOfAccounts" + "ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes" + "ChartOfCalculationTypes"="ChartsOfCalculationTypes" + "BusinessProcess"="BusinessProcesses"; "Task"="Tasks" + "InformationRegister"="InformationRegisters"; "AccumulationRegister"="AccumulationRegisters" + "AccountingRegister"="AccountingRegisters"; "CalculationRegister"="CalculationRegisters" +} + +$parts = $ModulePath.Split(".") +if ($parts.Count -lt 2) { + Write-Error "Invalid ModulePath format: $ModulePath. Expected: Type.Name.Module or CommonModule.Name" + exit 1 +} + +$objType = $parts[0] +$objName = $parts[1] + +if (-not $typeDirMap.ContainsKey($objType)) { + Write-Error "Unknown object type: $objType" + exit 1 +} +$dirName = $typeDirMap[$objType] + +$bslFile = $null +if ($objType -eq "CommonModule") { + # CommonModule.X -> CommonModules/X/Ext/Module.bsl + $bslFile = Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) "Ext") "Module.bsl" +} elseif ($parts.Count -ge 4 -and $parts[2] -eq "Form") { + # Type.X.Form.Y -> Types/X/Forms/Y/Ext/Form/Module.bsl + $formName = $parts[3] + $bslFile = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) "Forms") $formName) "Ext") "Form") "Module.bsl" +} elseif ($parts.Count -ge 3) { + # Type.X.ObjectModule -> Types/X/Ext/ObjectModule.bsl + $moduleName = $parts[2] + $moduleFileName = switch ($moduleName) { + "ObjectModule" { "ObjectModule.bsl" } + "ManagerModule" { "ManagerModule.bsl" } + "RecordSetModule" { "RecordSetModule.bsl" } + "CommandModule" { "CommandModule.bsl" } + default { "$moduleName.bsl" } + } + $bslFile = Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) (Join-Path "Ext" $moduleFileName) +} else { + Write-Error "Invalid ModulePath format: $ModulePath. Expected: Type.Name.Module, Type.Name.Form.FormName, or CommonModule.Name" + exit 1 +} + +# --- Map InterceptorType to decorator --- +$decorator = switch ($InterceptorType) { + "Before" { "&Перед" } + "After" { "&После" } + "ModificationAndControl" { "&ИзменениеИКонтроль" } +} + +# --- Map Context to annotation --- +$contextAnnotation = switch ($Context) { + "НаСервере" { "&НаСервере" } + "НаКлиенте" { "&НаКлиенте" } + "НаСервереБезКонтекста" { "&НаСервереБезКонтекста" } + default { "&$Context" } +} + +# --- Procedure name --- +$procName = "${namePrefix}${MethodName}" + +# --- Generate BSL code --- +$keyword = if ($IsFunction) { "Функция" } else { "Процедура" } +$endKeyword = if ($IsFunction) { "КонецФункции" } else { "КонецПроцедуры" } + +$bodyLines = @() +switch ($InterceptorType) { + "Before" { + $bodyLines += "`t// TODO: код перед вызовом оригинального метода" + } + "After" { + $bodyLines += "`t// TODO: код после вызова оригинального метода" + } + "ModificationAndControl" { + $bodyLines += "`t// Скопируйте тело оригинального метода и внесите изменения," + $bodyLines += "`t// используя маркеры #Удаление / #КонецУдаления и #Вставка / #КонецВставки" + } +} + +if ($IsFunction) { + $bodyLines += "`t" + $bodyLines += "`tВозврат Неопределено; // TODO: заменить на реальное возвращаемое значение" +} + +$bslCode = @() +$bslCode += "$contextAnnotation" +$bslCode += "${decorator}(`"$MethodName`")" +$bslCode += "$keyword ${procName}()" +$bslCode += $bodyLines +$bslCode += "$endKeyword" + +$bslText = ($bslCode -join "`r`n") + "`r`n" + +# --- Check if file exists and append --- +$bslDir = Split-Path $bslFile -Parent +if (-not (Test-Path $bslDir)) { + New-Item -ItemType Directory -Path $bslDir -Force | Out-Null +} + +$enc = New-Object System.Text.UTF8Encoding($true) + +if (Test-Path $bslFile) { + # Append to existing file + $existing = [System.IO.File]::ReadAllText($bslFile, $enc) + $separator = "`r`n" + if ($existing -and -not $existing.EndsWith("`n")) { + $separator = "`r`n`r`n" + } + $newContent = $existing + $separator + $bslText + [System.IO.File]::WriteAllText($bslFile, $newContent, $enc) + Write-Host "[OK] Добавлен перехватчик в существующий файл" +} else { + [System.IO.File]::WriteAllText($bslFile, $bslText, $enc) + Write-Host "[OK] Создан файл модуля" +} + +Write-Host " Файл: $bslFile" +Write-Host " Декоратор: $decorator(`"$MethodName`")" +Write-Host " Процедура: ${procName}()" +Write-Host " Контекст: $contextAnnotation" diff --git a/.claude/skills/cfe-validate/SKILL.md b/.claude/skills/cfe-validate/SKILL.md new file mode 100644 index 00000000..af1b1067 --- /dev/null +++ b/.claude/skills/cfe-validate/SKILL.md @@ -0,0 +1,51 @@ +--- +name: cfe-validate +description: Валидация расширения конфигурации 1С (CFE). Используй после создания или модификации расширения для проверки корректности +argument-hint: [-MaxErrors 30] +allowed-tools: + - Bash + - Read + - Glob +--- + +# /cfe-validate — Валидация расширения конфигурации + +Проверяет структурную корректность расширения: XML-формат, свойства, состав, заимствованные объекты. Аналог `/cf-validate`, но для расширений. + +## Параметры + +| Параметр | Описание | По умолчанию | +|----------|----------|--------------| +| `ExtensionPath` | Путь к каталогу или Configuration.xml расширения (обязат.) | — | +| `MaxErrors` | Лимит ошибок | 30 | +| `OutFile` | Записать результат в файл | — | + +## Команда + +```powershell +powershell.exe -NoProfile -File .claude\skills\cfe-validate\scripts\cfe-validate.ps1 -ExtensionPath src +``` + +## Проверки (9 шагов) + +| # | Проверка | Уровень | +|---|----------|---------| +| 1 | XML well-formedness, MetaDataObject/Configuration, version | ERROR | +| 2 | InternalInfo: 7 ContainedObject, валидные ClassId | ERROR | +| 3 | Extension properties: ObjectBelonging=Adopted, Name, Purpose, NamePrefix, KeepMapping | ERROR | +| 4 | Enum-значения: ConfigurationExtensionCompatibilityMode, DefaultRunMode, ScriptVariant, InterfaceCompatibilityMode | ERROR | +| 5 | ChildObjects: валидные типы (44), нет дубликатов, каноничный порядок | ERROR/WARN | +| 6 | DefaultLanguage ссылается на Language в ChildObjects | ERROR | +| 7 | Файлы языков существуют | WARN | +| 8 | Каталоги объектов существуют | WARN | +| 9 | Заимствованные объекты: ObjectBelonging=Adopted, ExtendedConfigurationObject UUID | ERROR/WARN | + +## Пример вывода + +``` +=== Validation: Extension.МоёРасширение === +[OK] 1. Root structure: MetaDataObject/Configuration, version 2.17 +[OK] 2. InternalInfo: 7 ContainedObject, all ClassIds valid +... +=== Result: 0 errors, 0 warnings === +``` diff --git a/.claude/skills/cfe-validate/scripts/cfe-validate.ps1 b/.claude/skills/cfe-validate/scripts/cfe-validate.ps1 new file mode 100644 index 00000000..ab03fdd9 --- /dev/null +++ b/.claude/skills/cfe-validate/scripts/cfe-validate.ps1 @@ -0,0 +1,607 @@ +# cfe-validate v1.0 — Validate 1C configuration extension structure (CFE) +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)] + [string]$ExtensionPath, + + [int]$MaxErrors = 30, + + [string]$OutFile +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve path --- +if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) { + $ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath +} + +if (Test-Path $ExtensionPath -PathType Container) { + $candidate = Join-Path $ExtensionPath "Configuration.xml" + if (Test-Path $candidate) { + $ExtensionPath = $candidate + } else { + Write-Host "[ERROR] No Configuration.xml found in directory: $ExtensionPath" + exit 1 + } +} + +if (-not (Test-Path $ExtensionPath)) { + Write-Host "[ERROR] File not found: $ExtensionPath" + exit 1 +} + +$resolvedPath = (Resolve-Path $ExtensionPath).Path +$configDir = Split-Path $resolvedPath -Parent + +# --- Output infrastructure --- +$script:errors = 0 +$script:warnings = 0 +$script:stopped = $false +$script:output = New-Object System.Text.StringBuilder 8192 + +function Out-Line { + param([string]$msg) + $script:output.AppendLine($msg) | Out-Null +} + +function Report-OK { + param([string]$msg) + Out-Line "[OK] $msg" +} + +function Report-Error { + param([string]$msg) + $script:errors++ + Out-Line "[ERROR] $msg" + if ($script:errors -ge $MaxErrors) { + $script:stopped = $true + } +} + +function Report-Warn { + param([string]$msg) + $script:warnings++ + Out-Line "[WARN] $msg" +} + +$finalize = { + Out-Line "" + Out-Line "=== Result: $($script:errors) errors, $($script:warnings) warnings ===" + + $result = $script:output.ToString() + Write-Host $result + + if ($OutFile) { + $utf8Bom = New-Object System.Text.UTF8Encoding $true + [System.IO.File]::WriteAllText($OutFile, $result, $utf8Bom) + Write-Host "Written to: $OutFile" + } +} + +# --- Reference tables --- +$guidPattern = '^[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}$' +$identPattern = '^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_][A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$' + +# 7 fixed ClassIds for Configuration +$validClassIds = @( + "9cd510cd-abfc-11d4-9434-004095e12fc7", + "9fcd25a0-4822-11d4-9414-008048da11f9", + "e3687481-0a87-462c-a166-9f34594f9bba", + "9de14907-ec23-4a07-96f0-85521cb6b53b", + "51f2d5d8-ea4d-4064-8892-82951750031e", + "e68182ea-4237-4383-967f-90c1e3370bc7", + "fb282519-d103-4dd3-bc12-cb271d631dfc" +) + +# 44 types in canonical order +$childObjectTypes = @( + "Language","Subsystem","StyleItem","Style", + "CommonPicture","SessionParameter","Role","CommonTemplate", + "FilterCriterion","CommonModule","CommonAttribute","ExchangePlan", + "XDTOPackage","WebService","HTTPService","WSReference", + "EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption", + "FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup", + "Constant","CommonForm","Catalog","Document", + "DocumentNumerator","Sequence","DocumentJournal","Enum", + "Report","DataProcessor","InformationRegister","AccumulationRegister", + "ChartOfCharacteristicTypes","ChartOfAccounts","AccountingRegister", + "ChartOfCalculationTypes","CalculationRegister", + "BusinessProcess","Task","IntegrationService" +) + +# Type -> directory mapping +$childTypeDirMap = @{ + "Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles" + "CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles" + "CommonTemplate"="CommonTemplates"; "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules" + "CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"; "XDTOPackage"="XDTOPackages" + "WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences" + "EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs" + "SettingsStorage"="SettingsStorages"; "FunctionalOption"="FunctionalOptions" + "FunctionalOptionsParameter"="FunctionalOptionsParameters"; "DefinedType"="DefinedTypes" + "CommonCommand"="CommonCommands"; "CommandGroup"="CommandGroups"; "Constant"="Constants" + "CommonForm"="CommonForms"; "Catalog"="Catalogs"; "Document"="Documents" + "DocumentNumerator"="DocumentNumerators"; "Sequence"="Sequences" + "DocumentJournal"="DocumentJournals"; "Enum"="Enums"; "Report"="Reports" + "DataProcessor"="DataProcessors"; "InformationRegister"="InformationRegisters" + "AccumulationRegister"="AccumulationRegisters" + "ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes" + "ChartOfAccounts"="ChartsOfAccounts"; "AccountingRegister"="AccountingRegisters" + "ChartOfCalculationTypes"="ChartsOfCalculationTypes" + "CalculationRegister"="CalculationRegisters" + "BusinessProcess"="BusinessProcesses"; "Task"="Tasks" + "IntegrationService"="IntegrationServices" +} + +# Valid enum values for extension properties +$validEnumValues = @{ + "ConfigurationExtensionCompatibilityMode" = @("DontUse","Version8_1","Version8_2_13","Version8_2_16","Version8_3_1","Version8_3_2","Version8_3_3","Version8_3_4","Version8_3_5","Version8_3_6","Version8_3_7","Version8_3_8","Version8_3_9","Version8_3_10","Version8_3_11","Version8_3_12","Version8_3_13","Version8_3_14","Version8_3_15","Version8_3_16","Version8_3_17","Version8_3_18","Version8_3_19","Version8_3_20","Version8_3_21","Version8_3_22","Version8_3_23","Version8_3_24","Version8_3_25","Version8_3_26","Version8_3_27","Version8_3_28") + "DefaultRunMode" = @("ManagedApplication","OrdinaryApplication","Auto") + "ScriptVariant" = @("Russian","English") + "InterfaceCompatibilityMode" = @("Taxi","TaxiEnableVersion8_2","Version8_2") +} + +# --- 1. Parse XML --- +Out-Line "" + +$xmlDoc = $null +try { + $xmlDoc = New-Object System.Xml.XmlDocument + $xmlDoc.PreserveWhitespace = $false + $xmlDoc.Load($resolvedPath) +} catch { + Out-Line "=== Validation: Extension (parse failed) ===" + Out-Line "" + Report-Error "1. XML parse failed: $($_.Exception.Message)" + & $finalize + exit 1 +} + +# --- Register namespaces --- +$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) +$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") +$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") +$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") +$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance") +$ns.AddNamespace("xs", "http://www.w3.org/2001/XMLSchema") +$ns.AddNamespace("app", "http://v8.1c.ru/8.2/managed-application/core") + +$root = $xmlDoc.DocumentElement + +# --- Check 1: Root structure --- +$check1Ok = $true +$expectedNs = "http://v8.1c.ru/8.3/MDClasses" + +if ($root.LocalName -ne "MetaDataObject") { + Report-Error "1. Root element is '$($root.LocalName)', expected 'MetaDataObject'" + & $finalize + exit 1 +} + +if ($root.NamespaceURI -ne $expectedNs) { + Report-Error "1. Root namespace is '$($root.NamespaceURI)', expected '$expectedNs'" + $check1Ok = $false +} + +$version = $root.GetAttribute("version") +if (-not $version) { + Report-Warn "1. Missing version attribute on MetaDataObject" +} elseif ($version -ne "2.17" -and $version -ne "2.20") { + Report-Warn "1. Unusual version '$version' (expected 2.17 or 2.20)" +} + +# Must have Configuration child +$cfgNode = $null +foreach ($child in $root.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Configuration" -and $child.NamespaceURI -eq $expectedNs) { + $cfgNode = $child; break + } +} + +if (-not $cfgNode) { + Report-Error "1. No element found inside MetaDataObject" + & $finalize + exit 1 +} + +# UUID +$cfgUuid = $cfgNode.GetAttribute("uuid") +if (-not $cfgUuid) { + Report-Error "1. Missing uuid on " + $check1Ok = $false +} elseif ($cfgUuid -notmatch $guidPattern) { + Report-Error "1. Invalid uuid '$cfgUuid' on " + $check1Ok = $false +} + +# Get name early for header +$propsNode = $cfgNode.SelectSingleNode("md:Properties", $ns) +$nameNode = if ($propsNode) { $propsNode.SelectSingleNode("md:Name", $ns) } else { $null } +$objName = if ($nameNode -and $nameNode.InnerText) { $nameNode.InnerText } else { "(unknown)" } + +$script:output.Insert(0, "=== Validation: Extension.$objName ===$([Environment]::NewLine)") | Out-Null + +if ($check1Ok) { + Report-OK "1. Root structure: MetaDataObject/Configuration, version $version" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 2: InternalInfo --- +$internalInfo = $cfgNode.SelectSingleNode("md:InternalInfo", $ns) +$check2Ok = $true + +if (-not $internalInfo) { + Report-Error "2. InternalInfo: missing" +} else { + $contained = $internalInfo.SelectNodes("xr:ContainedObject", $ns) + if ($contained.Count -ne 7) { + Report-Warn "2. InternalInfo: expected 7 ContainedObject, found $($contained.Count)" + } + + $foundClassIds = @{} + foreach ($co in $contained) { + $classId = $co.SelectSingleNode("xr:ClassId", $ns) + $objectId = $co.SelectSingleNode("xr:ObjectId", $ns) + + if (-not $classId -or -not $classId.InnerText) { + Report-Error "2. ContainedObject missing ClassId" + $check2Ok = $false + continue + } + + $cid = $classId.InnerText + if ($validClassIds -notcontains $cid) { + Report-Error "2. Unknown ClassId: $cid" + $check2Ok = $false + } + + if ($foundClassIds.ContainsKey($cid)) { + Report-Error "2. Duplicate ClassId: $cid" + $check2Ok = $false + } + $foundClassIds[$cid] = $true + + if (-not $objectId -or -not $objectId.InnerText) { + Report-Error "2. ContainedObject missing ObjectId for ClassId $cid" + $check2Ok = $false + } elseif ($objectId.InnerText -notmatch $guidPattern) { + Report-Error "2. Invalid ObjectId '$($objectId.InnerText)' for ClassId $cid" + $check2Ok = $false + } + } + + $missingIds = @($validClassIds | Where-Object { -not $foundClassIds.ContainsKey($_) }) + if ($missingIds.Count -gt 0) { + Report-Warn "2. Missing ClassIds: $($missingIds.Count) of 7" + } + + if ($check2Ok) { + Report-OK "2. InternalInfo: $($contained.Count) ContainedObject, all ClassIds valid" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 3: Extension-specific properties --- +if (-not $propsNode) { + Report-Error "3. Properties block missing" +} else { + $check3Ok = $true + + # ObjectBelonging = Adopted + $obNode = $propsNode.SelectSingleNode("md:ObjectBelonging", $ns) + if (-not $obNode -or $obNode.InnerText -ne "Adopted") { + Report-Error "3. ObjectBelonging must be 'Adopted', got '$($obNode.InnerText)'" + $check3Ok = $false + } + + # Name + if (-not $nameNode -or -not $nameNode.InnerText) { + Report-Error "3. Name is missing or empty" + $check3Ok = $false + } else { + $nameVal = $nameNode.InnerText + if ($nameVal -notmatch $identPattern) { + Report-Error "3. Name '$nameVal' is not a valid 1C identifier" + $check3Ok = $false + } + } + + # ConfigurationExtensionPurpose + $purposeNode = $propsNode.SelectSingleNode("md:ConfigurationExtensionPurpose", $ns) + $validPurposes = @("Patch","Customization","AddOn") + if (-not $purposeNode -or -not $purposeNode.InnerText) { + Report-Error "3. ConfigurationExtensionPurpose is missing" + $check3Ok = $false + } elseif ($validPurposes -notcontains $purposeNode.InnerText) { + Report-Error "3. ConfigurationExtensionPurpose '$($purposeNode.InnerText)' invalid (expected: Patch, Customization, AddOn)" + $check3Ok = $false + } + + # NamePrefix + $prefixNode = $propsNode.SelectSingleNode("md:NamePrefix", $ns) + if (-not $prefixNode -or -not $prefixNode.InnerText) { + Report-Warn "3. NamePrefix is empty" + } + + # KeepMappingToExtendedConfigurationObjectsByIDs + $keepMapNode = $propsNode.SelectSingleNode("md:KeepMappingToExtendedConfigurationObjectsByIDs", $ns) + if (-not $keepMapNode) { + Report-Warn "3. KeepMappingToExtendedConfigurationObjectsByIDs is missing" + } + + # DefaultLanguage + $defLangNode = $propsNode.SelectSingleNode("md:DefaultLanguage", $ns) + $defLang = if ($defLangNode -and $defLangNode.InnerText) { $defLangNode.InnerText } else { "" } + + if ($check3Ok) { + $purposeVal = if ($purposeNode) { $purposeNode.InnerText } else { "?" } + $prefixVal = if ($prefixNode -and $prefixNode.InnerText) { $prefixNode.InnerText } else { "(empty)" } + Report-OK "3. Extension properties: Name=`"$objName`", Purpose=$purposeVal, Prefix=$prefixVal" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 4: Enum property values --- +if ($propsNode) { + $enumChecked = 0 + $check4Ok = $true + + foreach ($propName in $validEnumValues.Keys) { + $propNode = $propsNode.SelectSingleNode("md:$propName", $ns) + if ($propNode -and $propNode.InnerText) { + $val = $propNode.InnerText + $allowed = $validEnumValues[$propName] + if ($allowed -notcontains $val) { + Report-Error "4. Property '$propName' has invalid value '$val'" + $check4Ok = $false + } + $enumChecked++ + } + } + + if ($check4Ok) { + Report-OK "4. Property values: $enumChecked enum properties checked" + } +} else { + Report-Warn "4. No Properties block to check" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 5: ChildObjects — valid types, no duplicates, order --- +$childObjNode = $cfgNode.SelectSingleNode("md:ChildObjects", $ns) + +if (-not $childObjNode) { + Report-Error "5. ChildObjects block missing" +} else { + $check5Ok = $true + $totalCount = 0 + $typeCounts = @{} + $duplicates = @{} + $typeFirstIndex = @{} + $lastTypeOrder = -1 + $orderOk = $true + + foreach ($child in $childObjNode.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + $typeName = $child.LocalName + $objNameVal = $child.InnerText + + $typeIdx = $childObjectTypes.IndexOf($typeName) + if ($typeIdx -lt 0) { + Report-Error "5. Unknown type '$typeName' in ChildObjects" + $check5Ok = $false + } else { + if (-not $typeFirstIndex.ContainsKey($typeName)) { + $typeFirstIndex[$typeName] = $typeIdx + if ($typeIdx -lt $lastTypeOrder) { + Report-Warn "5. Type '$typeName' is out of canonical order (after type at position $lastTypeOrder)" + $orderOk = $false + } + $lastTypeOrder = $typeIdx + } + } + + if (-not $typeCounts.ContainsKey($typeName)) { $typeCounts[$typeName] = @{} } + if ($typeCounts[$typeName].ContainsKey($objNameVal)) { + if (-not $duplicates.ContainsKey("$typeName.$objNameVal")) { + Report-Error "5. Duplicate: $typeName.$objNameVal" + $duplicates["$typeName.$objNameVal"] = $true + $check5Ok = $false + } + } else { + $typeCounts[$typeName][$objNameVal] = $true + } + + $totalCount++ + } + + $typeCount = $typeCounts.Count + if ($check5Ok) { + $orderInfo = if ($orderOk) { ", order correct" } else { "" } + Report-OK "5. ChildObjects: $typeCount types, $totalCount objects${orderInfo}" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 6: DefaultLanguage references existing Language in ChildObjects --- +if ($defLang -and $childObjNode) { + $langName = $defLang + if ($langName.StartsWith("Language.")) { + $langName = $langName.Substring(9) + } + + $found = $false + foreach ($child in $childObjNode.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Language" -and $child.InnerText -eq $langName) { + $found = $true; break + } + } + + if ($found) { + Report-OK "6. DefaultLanguage `"$defLang`" found in ChildObjects" + } else { + Report-Error "6. DefaultLanguage `"$defLang`" not found in ChildObjects" + } +} else { + if (-not $defLang) { + Report-Warn "6. Cannot check DefaultLanguage (empty)" + } else { + Report-Warn "6. Cannot check DefaultLanguage (no ChildObjects)" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 7: Language files exist --- +if ($childObjNode) { + $langNames = @() + foreach ($child in $childObjNode.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Language") { + $langNames += $child.InnerText + } + } + + if ($langNames.Count -gt 0) { + $existCount = 0 + foreach ($ln in $langNames) { + $langFile = Join-Path (Join-Path $configDir "Languages") "$ln.xml" + if (Test-Path $langFile) { + $existCount++ + } else { + Report-Warn "7. Language file missing: Languages/$ln.xml" + } + } + if ($existCount -eq $langNames.Count) { + Report-OK "7. Language files: $existCount/$($langNames.Count) exist" + } + } else { + Report-Warn "7. No Language entries in ChildObjects" + } +} else { + Report-Warn "7. Cannot check language files (no ChildObjects)" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 8: Object directories exist --- +if ($childObjNode) { + $dirsToCheck = @{} + foreach ($child in $childObjNode.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + $typeName = $child.LocalName + if ($typeName -eq "Language") { continue } + if ($childTypeDirMap.ContainsKey($typeName)) { + $dirName = $childTypeDirMap[$typeName] + if (-not $dirsToCheck.ContainsKey($dirName)) { + $dirsToCheck[$dirName] = 0 + } + $dirsToCheck[$dirName] = $dirsToCheck[$dirName] + 1 + } + } + + $missingDirs = @() + foreach ($dir in $dirsToCheck.Keys) { + $dirPath = Join-Path $configDir $dir + if (-not (Test-Path $dirPath -PathType Container)) { + $missingDirs += "$dir ($($dirsToCheck[$dir]) objects)" + } + } + + if ($missingDirs.Count -eq 0) { + Report-OK "8. Object directories: $($dirsToCheck.Count) directories, all exist" + } else { + foreach ($md in $missingDirs) { + Report-Warn "8. Missing directory: $md" + } + } +} else { + Report-OK "8. Object directories: N/A" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 9: Borrowed objects validation --- +if ($childObjNode) { + $borrowedCount = 0 + $borrowedOk = 0 + $check9Ok = $true + + foreach ($child in $childObjNode.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + $typeName = $child.LocalName + $childName = $child.InnerText + if ($typeName -eq "Language") { continue } + + if (-not $childTypeDirMap.ContainsKey($typeName)) { continue } + $dirName = $childTypeDirMap[$typeName] + $objFile = Join-Path (Join-Path $configDir $dirName) "$childName.xml" + + if (-not (Test-Path $objFile)) { continue } + + # Parse object XML + $objDoc = $null + try { + $objDoc = New-Object System.Xml.XmlDocument + $objDoc.PreserveWhitespace = $false + $objDoc.Load($objFile) + } catch { + Report-Warn "9. Cannot parse $dirName/$childName.xml: $($_.Exception.Message)" + continue + } + + $objNs = New-Object System.Xml.XmlNamespaceManager($objDoc.NameTable) + $objNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") + $objNs.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") + + # Find the object element (Catalog, Document, etc.) + $objRoot = $objDoc.DocumentElement + $objEl = $null + foreach ($c in $objRoot.ChildNodes) { + if ($c.NodeType -eq 'Element') { $objEl = $c; break } + } + if (-not $objEl) { continue } + + $objProps = $objEl.SelectSingleNode("md:Properties", $objNs) + if (-not $objProps) { continue } + + $obNode = $objProps.SelectSingleNode("md:ObjectBelonging", $objNs) + if ($obNode -and $obNode.InnerText -eq "Adopted") { + $borrowedCount++ + + # Check ExtendedConfigurationObject + $extObj = $objProps.SelectSingleNode("md:ExtendedConfigurationObject", $objNs) + if (-not $extObj -or -not $extObj.InnerText) { + Report-Error "9. Borrowed ${typeName}.${childName}: missing ExtendedConfigurationObject" + $check9Ok = $false + } elseif ($extObj.InnerText -notmatch $guidPattern) { + Report-Error "9. Borrowed ${typeName}.${childName}: invalid ExtendedConfigurationObject UUID '$($extObj.InnerText)'" + $check9Ok = $false + } else { + $borrowedOk++ + } + } + + if ($script:stopped) { break } + } + + if ($borrowedCount -eq 0) { + Report-OK "9. Borrowed objects: none found" + } elseif ($check9Ok) { + Report-OK "9. Borrowed objects: $borrowedOk/$borrowedCount validated" + } +} + +# --- Final output --- +& $finalize + +if ($script:errors -gt 0) { + exit 1 +} +exit 0 diff --git a/.claude/skills/epf-bsp-init/SKILL.md b/.claude/skills/epf-bsp-init/SKILL.md index 230c9870..adeab0c2 100644 --- a/.claude/skills/epf-bsp-init/SKILL.md +++ b/.claude/skills/epf-bsp-init/SKILL.md @@ -204,5 +204,5 @@ allowed-tools: - Добавить ещё команду: `/epf-bsp-add-command` - Добавить форму: `/epf-add-form` -- Добавить макет: `/epf-add-template` +- Добавить макет: `/template-add` - Собрать EPF: `/epf-build` diff --git a/.claude/skills/epf-build/SKILL.md b/.claude/skills/epf-build/SKILL.md index 7f21d638..4c25b4b1 100644 --- a/.claude/skills/epf-build/SKILL.md +++ b/.claude/skills/epf-build/SKILL.md @@ -1,6 +1,6 @@ --- name: epf-build -description: Собрать внешнюю обработку 1С (EPF) из XML-исходников +description: Собрать внешнюю обработку 1С (EPF/ERF) из XML-исходников argument-hint: allowed-tools: - Bash @@ -11,7 +11,7 @@ allowed-tools: # /epf-build — Сборка обработки -Собирает EPF-файл из XML-исходников с помощью платформы 1С. +Собирает EPF-файл из XML-исходников с помощью платформы 1С. Та же команда CLI работает и для внешних отчётов (ERF) — см. `/erf-build`. ## Usage diff --git a/.claude/skills/epf-dump/SKILL.md b/.claude/skills/epf-dump/SKILL.md index 0c799a77..f5304581 100644 --- a/.claude/skills/epf-dump/SKILL.md +++ b/.claude/skills/epf-dump/SKILL.md @@ -1,6 +1,6 @@ --- name: epf-dump -description: Разобрать EPF-файл обработки 1С в XML-исходники +description: Разобрать EPF-файл обработки 1С (EPF/ERF) в XML-исходники argument-hint: allowed-tools: - Bash @@ -11,7 +11,7 @@ allowed-tools: # /epf-dump — Разборка обработки -Разбирает EPF-файл во XML-исходники с помощью платформы 1С (иерархический формат). +Разбирает EPF-файл во XML-исходники с помощью платформы 1С (иерархический формат). Та же команда CLI работает и для внешних отчётов (ERF) — см. `/erf-dump`. ## Usage diff --git a/.claude/skills/epf-init/SKILL.md b/.claude/skills/epf-init/SKILL.md index 0985d4ec..da87304b 100644 --- a/.claude/skills/epf-init/SKILL.md +++ b/.claude/skills/epf-init/SKILL.md @@ -50,5 +50,6 @@ pwsh -NoProfile -File .claude/skills/epf-init/scripts/init.ps1 -Name "" [- ## Дальнейшие шаги - Добавить форму: `/epf-add-form` -- Добавить макет: `/epf-add-template` +- Добавить макет: `/template-add` +- Добавить справку: `/help-add` - Собрать EPF: `/epf-build` diff --git a/.claude/skills/epf-remove-form/SKILL.md b/.claude/skills/epf-remove-form/SKILL.md deleted file mode 100644 index 70b09dfc..00000000 --- a/.claude/skills/epf-remove-form/SKILL.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -name: epf-remove-form -description: Удалить форму из внешней обработки 1С -argument-hint: -disable-model-invocation: true -allowed-tools: - - Bash - - Read - - Write - - Edit - - Glob - - Grep ---- - -# /epf-remove-form — Удаление формы - -Удаляет форму и убирает её регистрацию из корневого XML обработки. - -## Usage - -``` -/epf-remove-form -``` - -| Параметр | Обязательный | По умолчанию | Описание | -|---------------|:------------:|--------------|-------------------------------------| -| ProcessorName | да | — | Имя обработки | -| FormName | да | — | Имя формы для удаления | -| SrcDir | нет | `src` | Каталог исходников | - -## Команда - -```powershell -pwsh -NoProfile -File .claude/skills/epf-remove-form/scripts/remove-form.ps1 -ProcessorName "" -FormName "" [-SrcDir ""] -``` - -## Что удаляется - -``` -//Forms/.xml # Метаданные формы -//Forms// # Каталог формы (рекурсивно) -``` - -## Что модифицируется - -- `/.xml` — убирается `
` из `ChildObjects` -- Если удаляемая форма была DefaultForm — очищается значение DefaultForm \ No newline at end of file diff --git a/.claude/skills/epf-remove-template/SKILL.md b/.claude/skills/epf-remove-template/SKILL.md deleted file mode 100644 index 9e6fda1f..00000000 --- a/.claude/skills/epf-remove-template/SKILL.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -name: epf-remove-template -description: Удалить макет из внешней обработки 1С -argument-hint: -disable-model-invocation: true -allowed-tools: - - Bash - - Read - - Write - - Edit - - Glob - - Grep ---- - -# /epf-remove-template — Удаление макета - -Удаляет макет и убирает его регистрацию из корневого XML обработки. - -## Usage - -``` -/epf-remove-template -``` - -| Параметр | Обязательный | По умолчанию | Описание | -|---------------|:------------:|--------------|-------------------------------------| -| ProcessorName | да | — | Имя обработки | -| TemplateName | да | — | Имя макета для удаления | -| SrcDir | нет | `src` | Каталог исходников | - -## Команда - -```powershell -pwsh -NoProfile -File .claude/skills/epf-remove-template/scripts/remove-template.ps1 -ProcessorName "" -TemplateName "" [-SrcDir ""] -``` - -## Что удаляется - -``` -//Templates/.xml # Метаданные макета -//Templates// # Каталог макета (рекурсивно) -``` - -## Что модифицируется - -- `/.xml` — убирается `