From 1b6ab2f1442d7e07239d6e6d26f87c0c58c0dc8a Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Sun, 15 Feb 2026 12:00:13 +0300 Subject: [PATCH] Add configuration root skills (4 new cf-* skills) and guide New skills for working with root-level 1C configuration files: - cf-info: analyze configuration structure (3 modes: brief/overview/full) - cf-init: scaffold empty configuration (Configuration.xml, ConfigDumpInfo.xml, Languages/) - cf-validate: validate structural correctness (8 checks) - cf-edit: edit properties, ChildObjects, default roles (6 operations) Also adds docs/cf-guide.md and updates README and specs index. Co-Authored-By: Claude Opus 4.6 --- .claude/skills/cf-edit/SKILL.md | 58 ++ .claude/skills/cf-edit/reference.md | 63 ++ .claude/skills/cf-edit/scripts/cf-edit.ps1 | 523 +++++++++++++++++ .claude/skills/cf-info/SKILL.md | 50 ++ .claude/skills/cf-info/scripts/cf-info.ps1 | 387 +++++++++++++ .claude/skills/cf-init/SKILL.md | 59 ++ .claude/skills/cf-init/scripts/cf-init.ps1 | 226 ++++++++ .claude/skills/cf-validate/SKILL.md | 70 +++ .../cf-validate/scripts/cf-validate.ps1 | 538 ++++++++++++++++++ README.md | 7 + docs/1c-specs-index.md | 2 + docs/cf-guide.md | 182 ++++++ 12 files changed, 2165 insertions(+) create mode 100644 .claude/skills/cf-edit/SKILL.md create mode 100644 .claude/skills/cf-edit/reference.md create mode 100644 .claude/skills/cf-edit/scripts/cf-edit.ps1 create mode 100644 .claude/skills/cf-info/SKILL.md create mode 100644 .claude/skills/cf-info/scripts/cf-info.ps1 create mode 100644 .claude/skills/cf-init/SKILL.md create mode 100644 .claude/skills/cf-init/scripts/cf-init.ps1 create mode 100644 .claude/skills/cf-validate/SKILL.md create mode 100644 .claude/skills/cf-validate/scripts/cf-validate.ps1 create mode 100644 docs/cf-guide.md diff --git a/.claude/skills/cf-edit/SKILL.md b/.claude/skills/cf-edit/SKILL.md new file mode 100644 index 00000000..aeca4e72 --- /dev/null +++ b/.claude/skills/cf-edit/SKILL.md @@ -0,0 +1,58 @@ +--- +name: cf-edit +description: Точечное редактирование конфигурации 1С (Configuration.xml) — свойства, состав ChildObjects, роли по умолчанию +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..d603ec5a --- /dev/null +++ b/.claude/skills/cf-init/SKILL.md @@ -0,0 +1,59 @@ +--- +name: cf-init +description: Создать пустую конфигурацию 1С (scaffold XML-исходников) — Configuration.xml, ConfigDumpInfo.xml, Languages/ +argument-hint: [-Synonym ] [-OutputDir src] +allowed-tools: + - Bash + - Read + - Glob +--- + +# /cf-init — Создание пустой конфигурации 1С + +Создаёт scaffold исходников пустой конфигурации 1С: `Configuration.xml`, `ConfigDumpInfo.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 # Корневой файл — все свойства +├── ConfigDumpInfo.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..c6b5d8b7 --- /dev/null +++ b/.claude/skills/cf-init/scripts/cf-init.ps1 @@ -0,0 +1,226 @@ +# 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 + + + + Русский + + + +"@ + +# --- ConfigDumpInfo.xml --- +$dumpInfoXml = @" + + + + +"@ + +# --- 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) +$dumpInfoFile = Join-Path $OutputDir "ConfigDumpInfo.xml" +[System.IO.File]::WriteAllText($dumpInfoFile, $dumpInfoXml, $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 " ConfigDumpInfo.xml: $dumpInfoFile" +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..ce339c2a --- /dev/null +++ b/.claude/skills/cf-validate/SKILL.md @@ -0,0 +1,70 @@ +--- +name: cf-validate +description: Валидация структурной корректности конфигурации 1С (Configuration.xml) — корневая структура, свойства, состав, языки, каталоги +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/README.md b/README.md index 0b068f4b..c565b145 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ | Роли (Role) | 3 навыка `/role-*` | Анализ прав роли, создание из JSON DSL, валидация | [Подробнее](docs/role-guide.md) | | Схема компоновки (СКД) | 4 навыка `/skd-*` | Анализ, генерация из JSON DSL, точечное редактирование, валидация схем компоновки данных | [Подробнее](docs/skd-guide.md) | | Метаданные конфигурации | 4 навыка `/meta-*` | Создание, анализ, редактирование, валидация объектов метаданных (23 типа) | [Подробнее](docs/meta-guide.md) | +| Корневая конфигурация | 4 навыка `/cf-*` | Создание, анализ, редактирование, валидация корневых файлов конфигурации | [Подробнее](docs/cf-guide.md) | | Подсистемы (Subsystem) | 4 навыка `/subsystem-*` | Анализ, создание, редактирование, валидация подсистем конфигурации | — | | Командный интерфейс (CI) | 2 навыка `/interface-*` | Редактирование и валидация CommandInterface.xml подсистем | — | | Утилиты | `/img-grid` | Наложение сетки на изображение для определения пропорций колонок | — | @@ -54,6 +55,7 @@ - [SKD DSL](docs/skd-dsl-spec.md) — JSON-формат описания СКД для `/skd-compile` - [Объекты конфигурации](docs/1c-config-objects-spec.md) — XML-формат объектов метаданных конфигурации (23 типа) - [Подсистемы и командный интерфейс](docs/1c-subsystem-spec.md) — XML-формат подсистем, CommandInterface.xml, секции видимости/размещения/порядка +- [Корневая конфигурация](docs/1c-configuration-spec.md) — XML-формат Configuration.xml, ConfigDumpInfo.xml, Languages/, 44 типа ChildObjects ## Структура репозитория @@ -93,6 +95,10 @@ ├── meta-compile/ # Создание объекта метаданных ├── meta-edit/ # Редактирование объекта метаданных ├── meta-validate/ # Валидация объекта метаданных +├── cf-info/ # Анализ структуры конфигурации +├── cf-init/ # Создание пустой конфигурации +├── cf-edit/ # Редактирование конфигурации +├── cf-validate/ # Валидация конфигурации ├── subsystem-info/ # Анализ структуры подсистемы ├── subsystem-compile/ # Создание подсистемы из JSON ├── subsystem-edit/ # Редактирование подсистемы @@ -107,6 +113,7 @@ docs/ ├── role-guide.md # Гайд: роли ├── skd-guide.md # Гайд: схема компоновки данных ├── meta-guide.md # Гайд: объекты метаданных конфигурации +├── cf-guide.md # Гайд: корневые файлы конфигурации ├── 1c-epf-spec.md # Спецификация XML-формата (EPF) ├── 1c-erf-spec.md # Спецификация XML-формата (ERF) ├── 1c-form-spec.md # Спецификация управляемых форм diff --git a/docs/1c-specs-index.md b/docs/1c-specs-index.md index 1a0ed662..cb2301b7 100644 --- a/docs/1c-specs-index.md +++ b/docs/1c-specs-index.md @@ -13,6 +13,8 @@ | `Ext/` | Модули, интерфейс, начальная страница | [1c-configuration-spec.md § 4](1c-configuration-spec.md#4-ext--корневой-каталог-конфигурации) | | `Languages/` | Языки конфигурации | [1c-configuration-spec.md § 5](1c-configuration-spec.md#5-языки-languages) | +**Навыки:** `/cf-info` (анализ), `/cf-init` (создание), `/cf-validate` (валидация), `/cf-edit` (редактирование) + --- ## 2. Объекты метаданных diff --git a/docs/cf-guide.md b/docs/cf-guide.md new file mode 100644 index 00000000..518ba83e --- /dev/null +++ b/docs/cf-guide.md @@ -0,0 +1,182 @@ +# Корневые файлы конфигурации + +Навыки группы `/cf-*` позволяют создавать, анализировать, редактировать и проверять корневые файлы конфигурации 1С — `Configuration.xml`, `ConfigDumpInfo.xml`, `Languages/`. + +## Навыки + +| Навык | Параметры | Описание | +|-------|-----------|----------| +| `/cf-info` | ` [-Mode overview\|brief\|full]` | Анализ конфигурации: свойства, состав, счётчики объектов (3 режима) | +| `/cf-init` | ` [-Synonym] [-OutputDir] [-Version] [-Vendor]` | Создание пустой конфигурации (scaffold XML-исходников) | +| `/cf-validate` | ` [-MaxErrors 30]` | Валидация структурной корректности (8 проверок) | +| `/cf-edit` | ` -Operation -Value ""` | Редактирование свойств, состава ChildObjects, ролей по умолчанию (6 операций) | + +## Рабочий цикл + +``` +Описание (текст) → /cf-init → XML-исходники → /cf-validate + ↕ /cf-edit → /cf-info +``` + +1. `/cf-init` создаёт scaffold пустой конфигурации (Configuration.xml, ConfigDumpInfo.xml, Languages/) +2. `/cf-edit` вносит изменения: свойства, объекты в ChildObjects, роли по умолчанию +3. `/cf-validate` проверяет корректность XML (структура, enum-значения, ссылки, каталоги) +4. `/cf-info` выводит компактную сводку для визуальной проверки + +## cf-info — режимы вывода + +### brief — одна строка + +``` +Конфигурация: БухгалтерияПредприятия — "Бухгалтерия предприятия" v3.0.181.31 | 2847 объектов | Version8_3_24 +``` + +### overview (по умолчанию) — заголовок + ключевые свойства + счётчики + +``` +Конфигурация: ТестКонфигурация — "Тестовая конфигурация" + Version: 2.0.0.1 + Vendor: TestCompany + Compatibility: Version8_3_24 + DefaultLanguage: Language.Русский + +Объекты (4 шт.): + Language 1 + Role 1 + Catalog 1 + Document 1 +``` + +### full — все свойства + полный список объектов + +Выводит все свойства по категориям (скалярные, enum, ref), полный список ChildObjects поимённо, DefaultRoles и мобильные функциональности. + +## cf-edit — операции + +### Свойства + +```powershell +# Скалярные и enum +-Operation modify-property -Value "Version=2.0.0.1 ;; Vendor=Фирма 1С ;; CompatibilityMode=Version8_3_27" + +# Многоязычные (LocalString) +-Operation modify-property -Value "Synonym=Моя конфигурация ;; Copyright=ООО Фирма" + +# Ссылка +-Operation modify-property -Value "DefaultLanguage=Language.Русский" +``` + +Поддерживаемые свойства: + +| Категория | Свойства | +|-----------|----------| +| Скалярные | `Name`, `Version`, `Vendor`, `Comment`, `NamePrefix`, `UpdateCatalogAddress` | +| LocalString | `Synonym`, `BriefInformation`, `DetailedInformation`, `Copyright`, `VendorInformationAddress`, `ConfigurationInformationAddress` | +| Enum | `CompatibilityMode`, `ConfigurationExtensionCompatibilityMode`, `DefaultRunMode`, `ScriptVariant`, `DataLockControlMode`, `ObjectAutonumerationMode`, `ModalityUseMode`, `SynchronousPlatformExtensionAndAddInCallUseMode`, `InterfaceCompatibilityMode`, `DatabaseTablespacesUseMode`, `MainClientApplicationWindowMode` | +| Ref | `DefaultLanguage` | + +### Состав объектов (ChildObjects) + +```powershell +# Добавить (вставляется в каноническую позицию — по типу, затем по алфавиту) +-Operation add-childObject -Value "Catalog.Товары ;; Document.Заказ ;; Enum.ВидыОплат" + +# Удалить +-Operation remove-childObject -Value "Catalog.Устаревший" +``` + +44 типа объектов поддерживаются в каноническом порядке: Language, Subsystem, StyleItem, CommonPicture, ... IntegrationService. + +### Роли по умолчанию (DefaultRoles) + +```powershell +# Добавить +-Operation add-defaultRole -Value "ПолныеПрава" + +# Удалить +-Operation remove-defaultRole -Value "ПолныеПрава" + +# Заменить список целиком +-Operation set-defaultRoles -Value "ПолныеПрава ;; Администратор" +``` + +### JSON mode — комбинированные операции + +```json +[ + { "operation": "modify-property", "value": "Version=2.0.0.1 ;; Vendor=Test" }, + { "operation": "add-childObject", "value": "Catalog.Товары ;; Document.Заказ" }, + { "operation": "add-defaultRole", "value": "ПолныеПрава" } +] +``` + +## cf-validate — проверки + +| # | Проверка | Уровень | +|---|----------|---------| +| 1 | XML well-formedness, MetaDataObject/Configuration, version | ERROR | +| 2 | InternalInfo: 7 ContainedObject, валидные ClassId | ERROR | +| 3 | Properties: Name, Synonym, DefaultLanguage, DefaultRunMode | ERROR/WARN | +| 4 | Enum-значения (11 свойств) | ERROR | +| 5 | ChildObjects: валидные типы, нет дубликатов, порядок | ERROR/WARN | +| 6 | DefaultLanguage ссылается на существующий Language | ERROR | +| 7 | Файлы языков `Languages/.xml` существуют | WARN | +| 8 | Каталоги объектов из ChildObjects существуют | WARN | + +## Сценарии использования + +### Обзор существующей конфигурации + +``` +> Покажи структуру конфигурации C:\WS\cfsrc\acc_8.3.24 +``` + +Claude вызовет `/cf-info` и покажет: имя, синоним, версию, поставщика, количество объектов по типам. + +### Создание новой конфигурации + +``` +> Создай пустую конфигурацию МойПроект, версия 1.0.0.1, поставщик "ООО Ромашка" +``` + +Claude вызовет `/cf-init` → `/cf-edit` (Version, Vendor) → `/cf-validate` → `/cf-info`. + +### Добавление объектов в конфигурацию + +``` +> Добавь в конфигурацию src/ справочник Контрагенты, документ ЗаказКлиента и перечисление ВидыОплат +``` + +Claude вызовет `/cf-edit` с `add-childObject`, объекты встанут в каноническом порядке. + +### Проверка конфигурации после изменений + +``` +> Проверь корректность конфигурации src/ +``` + +Claude вызовет `/cf-validate` и покажет ошибки и предупреждения. + +## Структура корневых файлов + +``` +/ +├── Configuration.xml # Свойства и состав конфигурации +├── ConfigDumpInfo.xml # Служебный (версии объектов) +├── Ext/ # Модули конфигурации +│ ├── ManagedApplicationModule.bsl +│ ├── SessionModule.bsl +│ └── ... +└── Languages/ + └── Русский.xml # Язык конфигурации +``` + +## Связь с другими навыками + +- `/meta-compile` — при создании объекта автоматически регистрирует его в `Configuration.xml` (вызывает логику `add-childObject`) +- `/subsystem-edit` — при добавлении объекта в подсистему объект уже должен быть в ChildObjects +- `/cf-edit` + `/meta-compile` — типичная связка: сначала добавить объект в конфигурацию, затем создать его исходники + +## Спецификации + +- [1c-configuration-spec.md](1c-configuration-spec.md) — XML-формат Configuration.xml, ConfigDumpInfo.xml, Languages/, свойства, 44 типа ChildObjects