mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-07-06 03:08:57 +03:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f045304d46 |
@@ -1,63 +0,0 @@
|
|||||||
# 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`).
|
|
||||||
@@ -1,523 +0,0 @@
|
|||||||
# 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 <Configuration> 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</_W>"
|
|
||||||
$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<v8:item>`r`n$indent`t`t<v8:lang>ru</v8:lang>`r`n$indent`t`t<v8:content>$escaped</v8:content>`r`n$indent`t</v8:item>`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 <ChildObjects> 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 <ChildObjects> 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 <DefaultRoles> 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 = "<xr:Item xsi:type=`"xr:MDObjectRef`">$roleName</xr:Item>"
|
|
||||||
$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 <DefaultRoles> 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 <DefaultRoles> 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 = "<xr:Item xsi:type=`"xr:MDObjectRef`">$roleName</xr:Item>"
|
|
||||||
$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
|
|
||||||
@@ -1,516 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# cf-edit v1.0 — Edit 1C configuration root (Configuration.xml)
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
from html import escape as html_escape
|
|
||||||
from lxml import etree
|
|
||||||
|
|
||||||
MD_NS = "http://v8.1c.ru/8.3/MDClasses"
|
|
||||||
XR_NS = "http://v8.1c.ru/8.3/xcf/readable"
|
|
||||||
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
V8_NS = "http://v8.1c.ru/8.1/data/core"
|
|
||||||
XS_NS = "http://www.w3.org/2001/XMLSchema"
|
|
||||||
|
|
||||||
# Canonical type order for ChildObjects (44 types)
|
|
||||||
TYPE_ORDER = [
|
|
||||||
"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",
|
|
||||||
]
|
|
||||||
|
|
||||||
ML_PROPS = ["Synonym", "BriefInformation", "DetailedInformation", "Copyright", "VendorInformationAddress", "ConfigurationInformationAddress"]
|
|
||||||
SCALAR_PROPS = ["Name", "Version", "Vendor", "Comment", "NamePrefix", "UpdateCatalogAddress"]
|
|
||||||
REF_PROPS = ["DefaultLanguage"]
|
|
||||||
|
|
||||||
|
|
||||||
def localname(el):
|
|
||||||
return etree.QName(el.tag).localname
|
|
||||||
|
|
||||||
|
|
||||||
def info(msg):
|
|
||||||
print(f"[INFO] {msg}")
|
|
||||||
|
|
||||||
|
|
||||||
def warn(msg):
|
|
||||||
print(f"[WARN] {msg}")
|
|
||||||
|
|
||||||
|
|
||||||
def get_child_indent(container):
|
|
||||||
if container.text and "\n" in container.text:
|
|
||||||
after_nl = container.text.rsplit("\n", 1)[-1]
|
|
||||||
if after_nl and not after_nl.strip():
|
|
||||||
return after_nl
|
|
||||||
for child in container:
|
|
||||||
if child.tail and "\n" in child.tail:
|
|
||||||
after_nl = child.tail.rsplit("\n", 1)[-1]
|
|
||||||
if after_nl and not after_nl.strip():
|
|
||||||
return after_nl
|
|
||||||
depth = 0
|
|
||||||
current = container
|
|
||||||
while current is not None:
|
|
||||||
depth += 1
|
|
||||||
current = current.getparent()
|
|
||||||
return "\t" * depth
|
|
||||||
|
|
||||||
|
|
||||||
def insert_before_closing(container, new_el, child_indent):
|
|
||||||
children = list(container)
|
|
||||||
if len(children) == 0:
|
|
||||||
parent_indent = child_indent[:-1] if len(child_indent) > 0 else ""
|
|
||||||
container.text = "\r\n" + child_indent
|
|
||||||
new_el.tail = "\r\n" + parent_indent
|
|
||||||
container.append(new_el)
|
|
||||||
else:
|
|
||||||
last = children[-1]
|
|
||||||
new_el.tail = last.tail
|
|
||||||
last.tail = "\r\n" + child_indent
|
|
||||||
container.append(new_el)
|
|
||||||
|
|
||||||
|
|
||||||
def insert_before_ref(container, new_el, ref_el, child_indent):
|
|
||||||
"""Insert new_el before ref_el inside container."""
|
|
||||||
idx = list(container).index(ref_el)
|
|
||||||
prev = ref_el.getprevious()
|
|
||||||
if prev is not None:
|
|
||||||
new_el.tail = prev.tail
|
|
||||||
prev.tail = "\r\n" + child_indent
|
|
||||||
else:
|
|
||||||
new_el.tail = container.text
|
|
||||||
container.text = "\r\n" + child_indent
|
|
||||||
container.insert(idx, new_el)
|
|
||||||
|
|
||||||
|
|
||||||
def remove_with_indent(el):
|
|
||||||
parent = el.getparent()
|
|
||||||
prev = el.getprevious()
|
|
||||||
if prev is not None:
|
|
||||||
if el.tail:
|
|
||||||
prev.tail = el.tail
|
|
||||||
else:
|
|
||||||
if el.tail:
|
|
||||||
parent.text = el.tail
|
|
||||||
parent.remove(el)
|
|
||||||
|
|
||||||
|
|
||||||
def expand_self_closing(container, parent_indent):
|
|
||||||
if len(container) == 0 and not (container.text and container.text.strip()):
|
|
||||||
container.text = "\r\n" + parent_indent
|
|
||||||
|
|
||||||
|
|
||||||
def import_fragment(xml_string):
|
|
||||||
wrapper = (
|
|
||||||
f'<_W xmlns="{MD_NS}" xmlns:xsi="{XSI_NS}" xmlns:v8="{V8_NS}" '
|
|
||||||
f'xmlns:xr="{XR_NS}" xmlns:xs="{XS_NS}">{xml_string}</_W>'
|
|
||||||
)
|
|
||||||
frag = etree.fromstring(wrapper.encode("utf-8"))
|
|
||||||
return list(frag)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_batch_value(val):
|
|
||||||
items = []
|
|
||||||
for part in val.split(";;"):
|
|
||||||
trimmed = part.strip()
|
|
||||||
if trimmed:
|
|
||||||
items.append(trimmed)
|
|
||||||
return items
|
|
||||||
|
|
||||||
|
|
||||||
def save_xml_bom(tree, path):
|
|
||||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
|
||||||
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"')
|
|
||||||
with open(path, "wb") as f:
|
|
||||||
f.write(b"\xef\xbb\xbf")
|
|
||||||
f.write(xml_bytes)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(description="Edit 1C configuration root (Configuration.xml)", allow_abbrev=False)
|
|
||||||
parser.add_argument("-ConfigPath", required=True)
|
|
||||||
parser.add_argument("-DefinitionFile", default=None)
|
|
||||||
parser.add_argument("-Operation", default=None, choices=["modify-property", "add-childObject", "remove-childObject", "add-defaultRole", "remove-defaultRole", "set-defaultRoles"])
|
|
||||||
parser.add_argument("-Value", default=None)
|
|
||||||
parser.add_argument("-NoValidate", action="store_true")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
if args.DefinitionFile and args.Operation:
|
|
||||||
print("Cannot use both -DefinitionFile and -Operation", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
if not args.DefinitionFile and not args.Operation:
|
|
||||||
print("Either -DefinitionFile or -Operation is required", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
config_path = args.ConfigPath
|
|
||||||
if not os.path.isabs(config_path):
|
|
||||||
config_path = os.path.join(os.getcwd(), config_path)
|
|
||||||
if os.path.isdir(config_path):
|
|
||||||
candidate = os.path.join(config_path, "Configuration.xml")
|
|
||||||
if os.path.isfile(candidate):
|
|
||||||
config_path = candidate
|
|
||||||
else:
|
|
||||||
print("No Configuration.xml in directory", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
if not os.path.isfile(config_path):
|
|
||||||
print(f"File not found: {config_path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
resolved_path = os.path.abspath(config_path)
|
|
||||||
|
|
||||||
xml_parser = etree.XMLParser(remove_blank_text=False)
|
|
||||||
tree = etree.parse(resolved_path, xml_parser)
|
|
||||||
xml_root = tree.getroot()
|
|
||||||
|
|
||||||
add_count = 0
|
|
||||||
remove_count = 0
|
|
||||||
modify_count = 0
|
|
||||||
|
|
||||||
cfg_el = None
|
|
||||||
for child in xml_root:
|
|
||||||
if isinstance(child.tag, str) and localname(child) == "Configuration":
|
|
||||||
cfg_el = child
|
|
||||||
break
|
|
||||||
if cfg_el is None:
|
|
||||||
print("No <Configuration> element found", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
props_el = None
|
|
||||||
child_objs_el = None
|
|
||||||
for child in cfg_el:
|
|
||||||
if not isinstance(child.tag, str):
|
|
||||||
continue
|
|
||||||
if localname(child) == "Properties":
|
|
||||||
props_el = child
|
|
||||||
if localname(child) == "ChildObjects":
|
|
||||||
child_objs_el = child
|
|
||||||
|
|
||||||
obj_name = ""
|
|
||||||
if props_el is not None:
|
|
||||||
for child in props_el:
|
|
||||||
if isinstance(child.tag, str) and localname(child) == "Name":
|
|
||||||
obj_name = (child.text or "").strip()
|
|
||||||
break
|
|
||||||
info(f"Configuration: {obj_name}")
|
|
||||||
|
|
||||||
# --- Operations ---
|
|
||||||
def do_modify_property(batch_val):
|
|
||||||
nonlocal modify_count
|
|
||||||
items = parse_batch_value(batch_val)
|
|
||||||
for item in items:
|
|
||||||
eq_idx = item.find("=")
|
|
||||||
if eq_idx < 1:
|
|
||||||
print(f"Invalid property format '{item}', expected 'Key=Value'", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
prop_name = item[:eq_idx].strip()
|
|
||||||
prop_value = item[eq_idx + 1:].strip()
|
|
||||||
|
|
||||||
prop_el = None
|
|
||||||
for child in props_el:
|
|
||||||
if isinstance(child.tag, str) and localname(child) == prop_name:
|
|
||||||
prop_el = child
|
|
||||||
break
|
|
||||||
if prop_el is None:
|
|
||||||
print(f"Property '{prop_name}' not found in Properties", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if prop_name in ML_PROPS:
|
|
||||||
for ch in list(prop_el):
|
|
||||||
prop_el.remove(ch)
|
|
||||||
if not prop_value:
|
|
||||||
prop_el.text = None
|
|
||||||
else:
|
|
||||||
indent = get_child_indent(props_el)
|
|
||||||
item_el = etree.SubElement(prop_el, f"{{{V8_NS}}}item")
|
|
||||||
lang_el = etree.SubElement(item_el, f"{{{V8_NS}}}lang")
|
|
||||||
lang_el.text = "ru"
|
|
||||||
content_el = etree.SubElement(item_el, f"{{{V8_NS}}}content")
|
|
||||||
content_el.text = prop_value
|
|
||||||
prop_el.text = "\r\n" + indent + "\t"
|
|
||||||
item_el.text = "\r\n" + indent + "\t\t"
|
|
||||||
lang_el.tail = "\r\n" + indent + "\t\t"
|
|
||||||
content_el.tail = "\r\n" + indent + "\t"
|
|
||||||
item_el.tail = "\r\n" + indent
|
|
||||||
elif prop_name in SCALAR_PROPS or prop_name in REF_PROPS:
|
|
||||||
for ch in list(prop_el):
|
|
||||||
prop_el.remove(ch)
|
|
||||||
if not prop_value:
|
|
||||||
prop_el.text = None
|
|
||||||
else:
|
|
||||||
prop_el.text = prop_value
|
|
||||||
else:
|
|
||||||
for ch in list(prop_el):
|
|
||||||
prop_el.remove(ch)
|
|
||||||
prop_el.text = prop_value
|
|
||||||
|
|
||||||
modify_count += 1
|
|
||||||
info(f'Set {prop_name} = "{prop_value}"')
|
|
||||||
|
|
||||||
def do_add_child_object(batch_val):
|
|
||||||
nonlocal add_count
|
|
||||||
if child_objs_el is None:
|
|
||||||
print("No <ChildObjects> element found", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
items = parse_batch_value(batch_val)
|
|
||||||
cfg_indent = get_child_indent(cfg_el)
|
|
||||||
if len(child_objs_el) == 0 and not (child_objs_el.text and child_objs_el.text.strip()):
|
|
||||||
expand_self_closing(child_objs_el, cfg_indent)
|
|
||||||
child_indent = get_child_indent(child_objs_el)
|
|
||||||
|
|
||||||
for item in items:
|
|
||||||
dot_idx = item.find(".")
|
|
||||||
if dot_idx < 1:
|
|
||||||
print(f"Invalid format '{item}', expected 'Type.Name'", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
type_name = item[:dot_idx]
|
|
||||||
obj_name_val = item[dot_idx + 1:]
|
|
||||||
|
|
||||||
if type_name not in TYPE_ORDER:
|
|
||||||
print(f"Unknown type '{type_name}'", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
type_idx = TYPE_ORDER.index(type_name)
|
|
||||||
|
|
||||||
# Dedup
|
|
||||||
exists = False
|
|
||||||
for child in child_objs_el:
|
|
||||||
if isinstance(child.tag, str) and localname(child) == type_name and (child.text or "") == obj_name_val:
|
|
||||||
exists = True
|
|
||||||
break
|
|
||||||
if exists:
|
|
||||||
warn(f"Already exists: {type_name}.{obj_name_val}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Find insertion point
|
|
||||||
insert_before = None
|
|
||||||
for child in child_objs_el:
|
|
||||||
if not isinstance(child.tag, str):
|
|
||||||
continue
|
|
||||||
child_type_name = localname(child)
|
|
||||||
if child_type_name not in TYPE_ORDER:
|
|
||||||
continue
|
|
||||||
child_type_idx = TYPE_ORDER.index(child_type_name)
|
|
||||||
|
|
||||||
if child_type_name == type_name:
|
|
||||||
if (child.text or "") > obj_name_val and insert_before is None:
|
|
||||||
insert_before = child
|
|
||||||
elif child_type_idx > type_idx and insert_before is None:
|
|
||||||
insert_before = child
|
|
||||||
|
|
||||||
new_el = etree.Element(f"{{{MD_NS}}}{type_name}")
|
|
||||||
new_el.text = obj_name_val
|
|
||||||
|
|
||||||
if insert_before is not None:
|
|
||||||
insert_before_ref(child_objs_el, new_el, insert_before, child_indent)
|
|
||||||
else:
|
|
||||||
insert_before_closing(child_objs_el, new_el, child_indent)
|
|
||||||
|
|
||||||
add_count += 1
|
|
||||||
info(f"Added: {type_name}.{obj_name_val}")
|
|
||||||
|
|
||||||
def do_remove_child_object(batch_val):
|
|
||||||
nonlocal remove_count
|
|
||||||
if child_objs_el is None:
|
|
||||||
print("No <ChildObjects> element found", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
items = parse_batch_value(batch_val)
|
|
||||||
for item in items:
|
|
||||||
dot_idx = item.find(".")
|
|
||||||
if dot_idx < 1:
|
|
||||||
print(f"Invalid format '{item}', expected 'Type.Name'", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
type_name = item[:dot_idx]
|
|
||||||
obj_name_val = item[dot_idx + 1:]
|
|
||||||
|
|
||||||
found = False
|
|
||||||
for child in list(child_objs_el):
|
|
||||||
if isinstance(child.tag, str) and localname(child) == type_name and (child.text or "") == obj_name_val:
|
|
||||||
remove_with_indent(child)
|
|
||||||
remove_count += 1
|
|
||||||
info(f"Removed: {type_name}.{obj_name_val}")
|
|
||||||
found = True
|
|
||||||
break
|
|
||||||
if not found:
|
|
||||||
warn(f"Not found: {type_name}.{obj_name_val}")
|
|
||||||
|
|
||||||
def do_add_default_role(batch_val):
|
|
||||||
nonlocal add_count
|
|
||||||
items = parse_batch_value(batch_val)
|
|
||||||
|
|
||||||
roles_el = None
|
|
||||||
for child in props_el:
|
|
||||||
if isinstance(child.tag, str) and localname(child) == "DefaultRoles":
|
|
||||||
roles_el = child
|
|
||||||
break
|
|
||||||
if roles_el is None:
|
|
||||||
print("No <DefaultRoles> element found in Properties", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
props_indent = get_child_indent(props_el)
|
|
||||||
if len(roles_el) == 0 and not (roles_el.text and roles_el.text.strip()):
|
|
||||||
expand_self_closing(roles_el, props_indent)
|
|
||||||
role_indent = get_child_indent(roles_el)
|
|
||||||
|
|
||||||
for item in items:
|
|
||||||
role_name = item
|
|
||||||
if not role_name.startswith("Role."):
|
|
||||||
role_name = f"Role.{role_name}"
|
|
||||||
|
|
||||||
exists = False
|
|
||||||
for child in roles_el:
|
|
||||||
if isinstance(child.tag, str) and (child.text or "").strip() == role_name:
|
|
||||||
exists = True
|
|
||||||
break
|
|
||||||
if exists:
|
|
||||||
warn(f"DefaultRole already exists: {role_name}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
frag_xml = f'<xr:Item xsi:type="xr:MDObjectRef">{role_name}</xr:Item>'
|
|
||||||
nodes = import_fragment(frag_xml)
|
|
||||||
if nodes:
|
|
||||||
insert_before_closing(roles_el, nodes[0], role_indent)
|
|
||||||
add_count += 1
|
|
||||||
info(f"Added DefaultRole: {role_name}")
|
|
||||||
|
|
||||||
def do_remove_default_role(batch_val):
|
|
||||||
nonlocal remove_count
|
|
||||||
items = parse_batch_value(batch_val)
|
|
||||||
|
|
||||||
roles_el = None
|
|
||||||
for child in props_el:
|
|
||||||
if isinstance(child.tag, str) and localname(child) == "DefaultRoles":
|
|
||||||
roles_el = child
|
|
||||||
break
|
|
||||||
if roles_el is None:
|
|
||||||
print("No <DefaultRoles> element found", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
for item in items:
|
|
||||||
role_name = item
|
|
||||||
if not role_name.startswith("Role."):
|
|
||||||
role_name = f"Role.{role_name}"
|
|
||||||
|
|
||||||
found = False
|
|
||||||
for child in list(roles_el):
|
|
||||||
if isinstance(child.tag, str) and (child.text or "").strip() == role_name:
|
|
||||||
remove_with_indent(child)
|
|
||||||
remove_count += 1
|
|
||||||
info(f"Removed DefaultRole: {role_name}")
|
|
||||||
found = True
|
|
||||||
break
|
|
||||||
if not found:
|
|
||||||
warn(f"DefaultRole not found: {role_name}")
|
|
||||||
|
|
||||||
def do_set_default_roles(batch_val):
|
|
||||||
nonlocal modify_count
|
|
||||||
items = parse_batch_value(batch_val)
|
|
||||||
|
|
||||||
roles_el = None
|
|
||||||
for child in props_el:
|
|
||||||
if isinstance(child.tag, str) and localname(child) == "DefaultRoles":
|
|
||||||
roles_el = child
|
|
||||||
break
|
|
||||||
if roles_el is None:
|
|
||||||
print("No <DefaultRoles> element found", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Clear all existing children
|
|
||||||
for ch in list(roles_el):
|
|
||||||
roles_el.remove(ch)
|
|
||||||
roles_el.text = None
|
|
||||||
|
|
||||||
if not items:
|
|
||||||
modify_count += 1
|
|
||||||
info("Cleared DefaultRoles")
|
|
||||||
return
|
|
||||||
|
|
||||||
props_indent = get_child_indent(props_el)
|
|
||||||
role_indent = props_indent + "\t"
|
|
||||||
|
|
||||||
roles_el.text = "\r\n" + props_indent
|
|
||||||
|
|
||||||
for item in items:
|
|
||||||
role_name = item
|
|
||||||
if not role_name.startswith("Role."):
|
|
||||||
role_name = f"Role.{role_name}"
|
|
||||||
|
|
||||||
frag_xml = f'<xr:Item xsi:type="xr:MDObjectRef">{role_name}</xr:Item>'
|
|
||||||
nodes = import_fragment(frag_xml)
|
|
||||||
if nodes:
|
|
||||||
insert_before_closing(roles_el, nodes[0], role_indent)
|
|
||||||
|
|
||||||
modify_count += 1
|
|
||||||
info(f"Set DefaultRoles: {len(items)} roles")
|
|
||||||
|
|
||||||
# --- Execute operations ---
|
|
||||||
operations = []
|
|
||||||
if args.DefinitionFile:
|
|
||||||
def_file = args.DefinitionFile
|
|
||||||
if not os.path.isabs(def_file):
|
|
||||||
def_file = os.path.join(os.getcwd(), def_file)
|
|
||||||
with open(def_file, "r", encoding="utf-8-sig") as fh:
|
|
||||||
ops = json.loads(fh.read())
|
|
||||||
if isinstance(ops, list):
|
|
||||||
operations = ops
|
|
||||||
else:
|
|
||||||
operations = [ops]
|
|
||||||
else:
|
|
||||||
operations = [{"operation": args.Operation, "value": args.Value or ""}]
|
|
||||||
|
|
||||||
for op in operations:
|
|
||||||
op_name = op.get("operation", args.Operation or "")
|
|
||||||
op_value = op.get("value", args.Value or "")
|
|
||||||
|
|
||||||
if op_name == "modify-property":
|
|
||||||
do_modify_property(op_value)
|
|
||||||
elif op_name == "add-childObject":
|
|
||||||
do_add_child_object(op_value)
|
|
||||||
elif op_name == "remove-childObject":
|
|
||||||
do_remove_child_object(op_value)
|
|
||||||
elif op_name == "add-defaultRole":
|
|
||||||
do_add_default_role(op_value)
|
|
||||||
elif op_name == "remove-defaultRole":
|
|
||||||
do_remove_default_role(op_value)
|
|
||||||
elif op_name == "set-defaultRoles":
|
|
||||||
do_set_default_roles(op_value)
|
|
||||||
else:
|
|
||||||
print(f"Unknown operation: {op_name}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Save ---
|
|
||||||
save_xml_bom(tree, resolved_path)
|
|
||||||
info(f"Saved: {resolved_path}")
|
|
||||||
|
|
||||||
# --- Auto-validate ---
|
|
||||||
if not args.NoValidate:
|
|
||||||
validate_script = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "..", "cf-validate", "scripts", "cf-validate.py"))
|
|
||||||
if os.path.isfile(validate_script):
|
|
||||||
print()
|
|
||||||
print("--- Running cf-validate ---")
|
|
||||||
subprocess.run([sys.executable, validate_script, "-ConfigPath", resolved_path])
|
|
||||||
|
|
||||||
# --- Summary ---
|
|
||||||
print()
|
|
||||||
print("=== cf-edit summary ===")
|
|
||||||
print(f" Configuration: {obj_name}")
|
|
||||||
print(f" Added: {add_count}")
|
|
||||||
print(f" Removed: {remove_count}")
|
|
||||||
print(f" Modified: {modify_count}")
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
---
|
|
||||||
name: cf-validate
|
|
||||||
description: Валидация конфигурации 1С. Используй после создания или модификации конфигурации для проверки корректности
|
|
||||||
argument-hint: <ConfigPath> [-Detailed] [-MaxErrors 30]
|
|
||||||
allowed-tools:
|
|
||||||
- Bash
|
|
||||||
- Read
|
|
||||||
- Glob
|
|
||||||
---
|
|
||||||
|
|
||||||
# /cf-validate — валидация конфигурации 1С
|
|
||||||
|
|
||||||
Проверяет Configuration.xml на структурные ошибки: XML well-formedness, InternalInfo, свойства, enum-значения, ChildObjects, DefaultLanguage, файлы языков, каталоги объектов.
|
|
||||||
|
|
||||||
## Параметры
|
|
||||||
|
|
||||||
| Параметр | Обяз. | Умолч. | Описание |
|
|
||||||
|------------|:-----:|---------|-------------------------------------------------|
|
|
||||||
| ConfigPath | да | — | Путь к Configuration.xml или каталогу выгрузки |
|
|
||||||
| Detailed | нет | — | Показывать [OK] для каждой проверки |
|
|
||||||
| MaxErrors | нет | 30 | Остановиться после N ошибок |
|
|
||||||
| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) |
|
|
||||||
|
|
||||||
## Команда
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
powershell.exe -NoProfile -File .claude/skills/cf-validate/scripts/cf-validate.ps1 -ConfigPath "upload/cfempty"
|
|
||||||
powershell.exe -NoProfile -File .claude/skills/cf-validate/scripts/cf-validate.ps1 -ConfigPath "upload/cfempty/Configuration.xml"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Проверки
|
|
||||||
|
|
||||||
| # | Проверка | Серьёзность |
|
|
||||||
|---|----------|-------------|
|
|
||||||
| 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/<name>.xml существуют | WARN |
|
|
||||||
| 8 | Каталоги объектов из ChildObjects существуют (spot-check) | WARN |
|
|
||||||
|
|
||||||
Exit code: 0 = OK, 1 = есть ошибки. По умолчанию краткий вывод. `-Detailed` для поштучной детализации.
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
---
|
|
||||||
name: cfe-validate
|
|
||||||
description: Валидация расширения конфигурации 1С (CFE). Используй после создания или модификации расширения для проверки корректности
|
|
||||||
argument-hint: <ExtensionPath> [-Detailed] [-MaxErrors 30]
|
|
||||||
allowed-tools:
|
|
||||||
- Bash
|
|
||||||
- Read
|
|
||||||
- Glob
|
|
||||||
---
|
|
||||||
|
|
||||||
# /cfe-validate — валидация расширения конфигурации (CFE)
|
|
||||||
|
|
||||||
Проверяет структурную корректность расширения: XML-формат, свойства, состав, заимствованные объекты. Аналог `/cf-validate`, но для расширений.
|
|
||||||
|
|
||||||
## Параметры
|
|
||||||
|
|
||||||
| Параметр | Обяз. | Умолч. | Описание |
|
|
||||||
|---------------|:-----:|---------|-------------------------------------------------|
|
|
||||||
| ExtensionPath | да | — | Путь к каталогу или Configuration.xml расширения |
|
|
||||||
| Detailed | нет | — | Показывать [OK] для каждой проверки |
|
|
||||||
| MaxErrors | нет | 30 | Остановиться после N ошибок |
|
|
||||||
| OutFile | нет | — | Записать результат в файл |
|
|
||||||
|
|
||||||
## Команда
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
powershell.exe -NoProfile -File .claude/skills/cfe-validate/scripts/cfe-validate.ps1 -ExtensionPath "src"
|
|
||||||
powershell.exe -NoProfile -File .claude/skills/cfe-validate/scripts/cfe-validate.ps1 -ExtensionPath "src/Configuration.xml"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Проверки (13 шагов)
|
|
||||||
|
|
||||||
| # | Проверка | Уровень |
|
|
||||||
|---|----------|---------|
|
|
||||||
| 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 |
|
|
||||||
| 10 | Sub-items: Attribute, TabularSection (InternalInfo + вложенные), EnumValue, Form-ссылки | ERROR |
|
|
||||||
| 11 | Заимствованные формы: метаданные, Form.xml, Module.bsl, BaseForm version | ERROR/WARN |
|
|
||||||
| 12 | Зависимости форм: CommonPicture, StyleItem (с whitelist платформенных), Enum DesignTimeRef | WARN |
|
|
||||||
| 13 | TypeLink: human-readable Items.* DataPath (должны быть удалены) | WARN |
|
|
||||||
|
|
||||||
Exit code: 0 = OK, 1 = есть ошибки. По умолчанию краткий вывод. `-Detailed` для поштучной детализации.
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# db-create v1.0 — Create 1C information base
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import glob
|
|
||||||
import os
|
|
||||||
import random
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
|
||||||
"""Resolve path to 1cv8.exe."""
|
|
||||||
if not v8path:
|
|
||||||
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
|
|
||||||
if found:
|
|
||||||
return found[-1]
|
|
||||||
else:
|
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
elif os.path.isdir(v8path):
|
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
|
||||||
|
|
||||||
if not os.path.isfile(v8path):
|
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
return v8path
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Create 1C information base",
|
|
||||||
allow_abbrev=False,
|
|
||||||
)
|
|
||||||
parser.add_argument("-V8Path", default="")
|
|
||||||
parser.add_argument("-InfoBasePath", default="")
|
|
||||||
parser.add_argument("-InfoBaseServer", default="")
|
|
||||||
parser.add_argument("-InfoBaseRef", default="")
|
|
||||||
parser.add_argument("-UseTemplate", default="")
|
|
||||||
parser.add_argument("-AddToList", action="store_true")
|
|
||||||
parser.add_argument("-ListName", default="")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
|
||||||
|
|
||||||
# --- Validate connection ---
|
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Validate template ---
|
|
||||||
if args.UseTemplate and not os.path.exists(args.UseTemplate):
|
|
||||||
print(f"Error: template file not found: {args.UseTemplate}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_create_{random.randint(0, 999999)}")
|
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# --- Build arguments ---
|
|
||||||
arguments = ["CREATEINFOBASE"]
|
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
|
||||||
arguments.append(f'Srvr="{args.InfoBaseServer}";Ref="{args.InfoBaseRef}"')
|
|
||||||
else:
|
|
||||||
arguments.append(f'File="{args.InfoBasePath}"')
|
|
||||||
|
|
||||||
# --- Template ---
|
|
||||||
if args.UseTemplate:
|
|
||||||
arguments.extend(["/UseTemplate", args.UseTemplate])
|
|
||||||
|
|
||||||
# --- Add to list ---
|
|
||||||
if args.AddToList:
|
|
||||||
if args.ListName:
|
|
||||||
arguments.extend(["/AddToList", args.ListName])
|
|
||||||
else:
|
|
||||||
arguments.append("/AddToList")
|
|
||||||
|
|
||||||
# --- Output ---
|
|
||||||
out_file = os.path.join(temp_dir, "create_log.txt")
|
|
||||||
arguments.extend(["/Out", out_file])
|
|
||||||
arguments.append("/DisableStartupDialogs")
|
|
||||||
|
|
||||||
# --- Execute ---
|
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
|
||||||
result = subprocess.run(
|
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
|
||||||
|
|
||||||
# --- Result ---
|
|
||||||
if exit_code == 0:
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
|
||||||
print(f"Information base created successfully: {args.InfoBaseServer}/{args.InfoBaseRef}")
|
|
||||||
else:
|
|
||||||
print(f"Information base created successfully: {args.InfoBasePath}")
|
|
||||||
else:
|
|
||||||
print(f"Error creating information base (code: {exit_code})", file=sys.stderr)
|
|
||||||
|
|
||||||
if os.path.isfile(out_file):
|
|
||||||
try:
|
|
||||||
with open(out_file, "r", encoding="utf-8-sig") as f:
|
|
||||||
log_content = f.read()
|
|
||||||
if log_content:
|
|
||||||
print("--- Log ---")
|
|
||||||
print(log_content)
|
|
||||||
print("--- End ---")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
sys.exit(exit_code)
|
|
||||||
|
|
||||||
finally:
|
|
||||||
if os.path.isdir(temp_dir):
|
|
||||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# db-dump-cf v1.0 — Dump 1C configuration to CF file
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import glob
|
|
||||||
import os
|
|
||||||
import random
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
|
||||||
"""Resolve path to 1cv8.exe."""
|
|
||||||
if not v8path:
|
|
||||||
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
|
|
||||||
if found:
|
|
||||||
return found[-1]
|
|
||||||
else:
|
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
elif os.path.isdir(v8path):
|
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
|
||||||
|
|
||||||
if not os.path.isfile(v8path):
|
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
return v8path
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Dump 1C configuration to CF file",
|
|
||||||
allow_abbrev=False,
|
|
||||||
)
|
|
||||||
parser.add_argument("-V8Path", default="")
|
|
||||||
parser.add_argument("-InfoBasePath", default="")
|
|
||||||
parser.add_argument("-InfoBaseServer", default="")
|
|
||||||
parser.add_argument("-InfoBaseRef", default="")
|
|
||||||
parser.add_argument("-UserName", default="")
|
|
||||||
parser.add_argument("-Password", default="")
|
|
||||||
parser.add_argument("-OutputFile", required=True)
|
|
||||||
parser.add_argument("-Extension", default="")
|
|
||||||
parser.add_argument("-AllExtensions", action="store_true")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
|
||||||
|
|
||||||
# --- Validate connection ---
|
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Ensure output directory exists ---
|
|
||||||
out_dir = os.path.dirname(args.OutputFile)
|
|
||||||
if out_dir and not os.path.isdir(out_dir):
|
|
||||||
os.makedirs(out_dir, exist_ok=True)
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_cf_{random.randint(0, 999999)}")
|
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# --- Build arguments ---
|
|
||||||
arguments = ["DESIGNER"]
|
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
|
||||||
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
|
|
||||||
else:
|
|
||||||
arguments.extend(["/F", args.InfoBasePath])
|
|
||||||
|
|
||||||
if args.UserName:
|
|
||||||
arguments.append(f"/N{args.UserName}")
|
|
||||||
if args.Password:
|
|
||||||
arguments.append(f"/P{args.Password}")
|
|
||||||
|
|
||||||
arguments.extend(["/DumpCfg", args.OutputFile])
|
|
||||||
|
|
||||||
# --- Extensions ---
|
|
||||||
if args.Extension:
|
|
||||||
arguments.extend(["-Extension", args.Extension])
|
|
||||||
elif args.AllExtensions:
|
|
||||||
arguments.append("-AllExtensions")
|
|
||||||
|
|
||||||
# --- Output ---
|
|
||||||
out_file = os.path.join(temp_dir, "dump_cf_log.txt")
|
|
||||||
arguments.extend(["/Out", out_file])
|
|
||||||
arguments.append("/DisableStartupDialogs")
|
|
||||||
|
|
||||||
# --- Execute ---
|
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
|
||||||
result = subprocess.run(
|
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
|
||||||
|
|
||||||
# --- Result ---
|
|
||||||
if exit_code == 0:
|
|
||||||
print(f"Configuration dumped successfully to: {args.OutputFile}")
|
|
||||||
else:
|
|
||||||
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
|
|
||||||
|
|
||||||
if os.path.isfile(out_file):
|
|
||||||
try:
|
|
||||||
with open(out_file, "r", encoding="utf-8-sig") as f:
|
|
||||||
log_content = f.read()
|
|
||||||
if log_content:
|
|
||||||
print("--- Log ---")
|
|
||||||
print(log_content)
|
|
||||||
print("--- End ---")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
sys.exit(exit_code)
|
|
||||||
|
|
||||||
finally:
|
|
||||||
if os.path.isdir(temp_dir):
|
|
||||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# db-dump-xml v1.0 — Dump 1C configuration to XML files
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import glob
|
|
||||||
import os
|
|
||||||
import random
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
|
||||||
"""Resolve path to 1cv8.exe."""
|
|
||||||
if not v8path:
|
|
||||||
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
|
||||||
if candidates:
|
|
||||||
candidates.sort()
|
|
||||||
return candidates[-1]
|
|
||||||
else:
|
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
elif os.path.isdir(v8path):
|
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
|
||||||
|
|
||||||
if not os.path.isfile(v8path):
|
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
return v8path
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Dump 1C configuration to XML files",
|
|
||||||
allow_abbrev=False,
|
|
||||||
)
|
|
||||||
parser.add_argument("-V8Path", default="", help="Path to 1cv8.exe or its bin directory")
|
|
||||||
parser.add_argument("-InfoBasePath", default="", help="Path to file infobase")
|
|
||||||
parser.add_argument("-InfoBaseServer", default="", help="1C server (for server infobase)")
|
|
||||||
parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server")
|
|
||||||
parser.add_argument("-UserName", default="", help="1C user name")
|
|
||||||
parser.add_argument("-Password", default="", help="1C user password")
|
|
||||||
parser.add_argument("-ConfigDir", required=True, help="Directory for configuration dump")
|
|
||||||
parser.add_argument(
|
|
||||||
"-Mode",
|
|
||||||
default="Changes",
|
|
||||||
choices=["Full", "Changes", "Partial", "UpdateInfo"],
|
|
||||||
help="Dump mode (default: Changes)",
|
|
||||||
)
|
|
||||||
parser.add_argument("-Objects", default="", help="Comma-separated metadata object names (for Partial mode)")
|
|
||||||
parser.add_argument("-Extension", default="", help="Extension name to dump")
|
|
||||||
parser.add_argument("-AllExtensions", action="store_true", help="Dump all extensions")
|
|
||||||
parser.add_argument(
|
|
||||||
"-Format",
|
|
||||||
default="Hierarchical",
|
|
||||||
choices=["Hierarchical", "Plain"],
|
|
||||||
help="Dump format (default: Hierarchical)",
|
|
||||||
)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
|
||||||
|
|
||||||
# --- Validate connection ---
|
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Validate Partial mode ---
|
|
||||||
if args.Mode == "Partial" and not args.Objects:
|
|
||||||
print("Error: -Objects required for Partial mode", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Create output dir if needed ---
|
|
||||||
if not os.path.exists(args.ConfigDir):
|
|
||||||
os.makedirs(args.ConfigDir, exist_ok=True)
|
|
||||||
print(f"Created output directory: {args.ConfigDir}")
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_xml_{random.randint(0, 999999)}")
|
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# --- Build arguments ---
|
|
||||||
arguments = ["DESIGNER"]
|
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
|
||||||
arguments += ["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"]
|
|
||||||
else:
|
|
||||||
arguments += ["/F", args.InfoBasePath]
|
|
||||||
|
|
||||||
if args.UserName:
|
|
||||||
arguments.append(f"/N{args.UserName}")
|
|
||||||
if args.Password:
|
|
||||||
arguments.append(f"/P{args.Password}")
|
|
||||||
|
|
||||||
arguments += ["/DumpConfigToFiles", args.ConfigDir]
|
|
||||||
arguments += ["-Format", args.Format]
|
|
||||||
|
|
||||||
if args.Mode == "Full":
|
|
||||||
print("Executing full configuration dump...")
|
|
||||||
elif args.Mode == "Changes":
|
|
||||||
print("Executing incremental configuration dump...")
|
|
||||||
arguments.append("-update")
|
|
||||||
arguments.append("-force")
|
|
||||||
elif args.Mode == "Partial":
|
|
||||||
print("Executing partial configuration dump...")
|
|
||||||
object_list = [obj.strip() for obj in args.Objects.split(",") if obj.strip()]
|
|
||||||
|
|
||||||
list_file = os.path.join(temp_dir, "dump_list.txt")
|
|
||||||
with open(list_file, "w", encoding="utf-8-sig") as f:
|
|
||||||
f.write("\n".join(object_list))
|
|
||||||
|
|
||||||
arguments += ["-listFile", list_file]
|
|
||||||
print(f"Objects to dump: {len(object_list)}")
|
|
||||||
for obj in object_list:
|
|
||||||
print(f" {obj}")
|
|
||||||
elif args.Mode == "UpdateInfo":
|
|
||||||
print("Updating ConfigDumpInfo.xml...")
|
|
||||||
arguments.append("-configDumpInfoOnly")
|
|
||||||
|
|
||||||
# --- Extensions ---
|
|
||||||
if args.Extension:
|
|
||||||
arguments += ["-Extension", args.Extension]
|
|
||||||
elif args.AllExtensions:
|
|
||||||
arguments.append("-AllExtensions")
|
|
||||||
|
|
||||||
# --- Output ---
|
|
||||||
out_file = os.path.join(temp_dir, "dump_log.txt")
|
|
||||||
arguments += ["/Out", out_file]
|
|
||||||
arguments.append("/DisableStartupDialogs")
|
|
||||||
|
|
||||||
# --- Execute ---
|
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
|
||||||
result = subprocess.run(
|
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
|
||||||
|
|
||||||
# --- Result ---
|
|
||||||
if exit_code == 0:
|
|
||||||
print("Dump completed successfully")
|
|
||||||
print(f"Configuration dumped to: {args.ConfigDir}")
|
|
||||||
else:
|
|
||||||
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
|
|
||||||
|
|
||||||
if os.path.isfile(out_file):
|
|
||||||
try:
|
|
||||||
with open(out_file, "r", encoding="utf-8-sig") as f:
|
|
||||||
log_content = f.read()
|
|
||||||
if log_content:
|
|
||||||
print("--- Log ---")
|
|
||||||
print(log_content)
|
|
||||||
print("--- End ---")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
sys.exit(exit_code)
|
|
||||||
|
|
||||||
finally:
|
|
||||||
if os.path.exists(temp_dir):
|
|
||||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# db-load-cf v1.0 — Load 1C configuration from CF file
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import glob
|
|
||||||
import os
|
|
||||||
import random
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
|
||||||
"""Resolve path to 1cv8.exe."""
|
|
||||||
if not v8path:
|
|
||||||
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
|
|
||||||
if found:
|
|
||||||
return found[-1]
|
|
||||||
else:
|
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
elif os.path.isdir(v8path):
|
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
|
||||||
|
|
||||||
if not os.path.isfile(v8path):
|
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
return v8path
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Load 1C configuration from CF file",
|
|
||||||
allow_abbrev=False,
|
|
||||||
)
|
|
||||||
parser.add_argument("-V8Path", default="")
|
|
||||||
parser.add_argument("-InfoBasePath", default="")
|
|
||||||
parser.add_argument("-InfoBaseServer", default="")
|
|
||||||
parser.add_argument("-InfoBaseRef", default="")
|
|
||||||
parser.add_argument("-UserName", default="")
|
|
||||||
parser.add_argument("-Password", default="")
|
|
||||||
parser.add_argument("-InputFile", required=True)
|
|
||||||
parser.add_argument("-Extension", default="")
|
|
||||||
parser.add_argument("-AllExtensions", action="store_true")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
|
||||||
|
|
||||||
# --- Validate connection ---
|
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Validate input file ---
|
|
||||||
if not os.path.isfile(args.InputFile):
|
|
||||||
print(f"Error: input file not found: {args.InputFile}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_cf_{random.randint(0, 999999)}")
|
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# --- Build arguments ---
|
|
||||||
arguments = ["DESIGNER"]
|
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
|
||||||
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
|
|
||||||
else:
|
|
||||||
arguments.extend(["/F", args.InfoBasePath])
|
|
||||||
|
|
||||||
if args.UserName:
|
|
||||||
arguments.append(f"/N{args.UserName}")
|
|
||||||
if args.Password:
|
|
||||||
arguments.append(f"/P{args.Password}")
|
|
||||||
|
|
||||||
arguments.extend(["/LoadCfg", args.InputFile])
|
|
||||||
|
|
||||||
# --- Extensions ---
|
|
||||||
if args.Extension:
|
|
||||||
arguments.extend(["-Extension", args.Extension])
|
|
||||||
elif args.AllExtensions:
|
|
||||||
arguments.append("-AllExtensions")
|
|
||||||
|
|
||||||
# --- Output ---
|
|
||||||
out_file = os.path.join(temp_dir, "load_cf_log.txt")
|
|
||||||
arguments.extend(["/Out", out_file])
|
|
||||||
arguments.append("/DisableStartupDialogs")
|
|
||||||
|
|
||||||
# --- Execute ---
|
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
|
||||||
result = subprocess.run(
|
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
|
||||||
|
|
||||||
# --- Result ---
|
|
||||||
if exit_code == 0:
|
|
||||||
print(f"Configuration loaded successfully from: {args.InputFile}")
|
|
||||||
else:
|
|
||||||
print(f"Error loading configuration (code: {exit_code})", file=sys.stderr)
|
|
||||||
|
|
||||||
if os.path.isfile(out_file):
|
|
||||||
try:
|
|
||||||
with open(out_file, "r", encoding="utf-8-sig") as f:
|
|
||||||
log_content = f.read()
|
|
||||||
if log_content:
|
|
||||||
print("--- Log ---")
|
|
||||||
print(log_content)
|
|
||||||
print("--- End ---")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
sys.exit(exit_code)
|
|
||||||
|
|
||||||
finally:
|
|
||||||
if os.path.isdir(temp_dir):
|
|
||||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,238 +0,0 @@
|
|||||||
# db-load-xml v1.1 — Load 1C configuration from XML files
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
<#
|
|
||||||
.SYNOPSIS
|
|
||||||
Загрузка конфигурации 1С из XML-файлов
|
|
||||||
|
|
||||||
.DESCRIPTION
|
|
||||||
Загружает конфигурацию в информационную базу из XML-файлов.
|
|
||||||
Поддерживает полную и частичную загрузку.
|
|
||||||
|
|
||||||
.PARAMETER V8Path
|
|
||||||
Путь к каталогу bin платформы или к 1cv8.exe
|
|
||||||
|
|
||||||
.PARAMETER InfoBasePath
|
|
||||||
Путь к файловой информационной базе
|
|
||||||
|
|
||||||
.PARAMETER InfoBaseServer
|
|
||||||
Сервер 1С (для серверной базы)
|
|
||||||
|
|
||||||
.PARAMETER InfoBaseRef
|
|
||||||
Имя базы на сервере
|
|
||||||
|
|
||||||
.PARAMETER UserName
|
|
||||||
Имя пользователя 1С
|
|
||||||
|
|
||||||
.PARAMETER Password
|
|
||||||
Пароль пользователя
|
|
||||||
|
|
||||||
.PARAMETER ConfigDir
|
|
||||||
Каталог XML-исходников конфигурации
|
|
||||||
|
|
||||||
.PARAMETER Mode
|
|
||||||
Режим загрузки: Full или Partial (по умолчанию Full)
|
|
||||||
|
|
||||||
.PARAMETER Files
|
|
||||||
Относительные пути файлов через запятую (для режима Partial)
|
|
||||||
|
|
||||||
.PARAMETER ListFile
|
|
||||||
Путь к файлу со списком файлов (альтернатива -Files, для режима Partial)
|
|
||||||
|
|
||||||
.PARAMETER Extension
|
|
||||||
Имя расширения для загрузки
|
|
||||||
|
|
||||||
.PARAMETER AllExtensions
|
|
||||||
Загрузить все расширения
|
|
||||||
|
|
||||||
.PARAMETER Format
|
|
||||||
Формат файлов: Hierarchical или Plain (по умолчанию Hierarchical)
|
|
||||||
|
|
||||||
.EXAMPLE
|
|
||||||
.\db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Full
|
|
||||||
|
|
||||||
.EXAMPLE
|
|
||||||
.\db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl"
|
|
||||||
#>
|
|
||||||
|
|
||||||
[CmdletBinding()]
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$V8Path,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$InfoBasePath,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$InfoBaseServer,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$InfoBaseRef,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$UserName,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$Password,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$true)]
|
|
||||||
[string]$ConfigDir,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[ValidateSet("Full", "Partial")]
|
|
||||||
[string]$Mode = "Full",
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$Files,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$ListFile,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$Extension,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[switch]$AllExtensions,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[ValidateSet("Hierarchical", "Plain")]
|
|
||||||
[string]$Format = "Hierarchical",
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[switch]$UpdateDB
|
|
||||||
)
|
|
||||||
|
|
||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
|
||||||
if ($found) {
|
|
||||||
$V8Path = $found.FullName
|
|
||||||
} else {
|
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
} elseif (Test-Path $V8Path -PathType Container) {
|
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Validate connection ---
|
|
||||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Validate config dir ---
|
|
||||||
if (-not (Test-Path $ConfigDir)) {
|
|
||||||
Write-Host "Error: config directory not found: $ConfigDir" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Validate Partial mode ---
|
|
||||||
if ($Mode -eq "Partial" -and -not $Files -and -not $ListFile) {
|
|
||||||
Write-Host "Error: -Files or -ListFile required for Partial mode" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
|
||||||
$tempDir = Join-Path $env:TEMP "db_load_xml_$(Get-Random)"
|
|
||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
|
||||||
|
|
||||||
try {
|
|
||||||
# --- Build arguments ---
|
|
||||||
$arguments = @("DESIGNER")
|
|
||||||
|
|
||||||
if ($InfoBaseServer -and $InfoBaseRef) {
|
|
||||||
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
|
|
||||||
} else {
|
|
||||||
$arguments += "/F", "`"$InfoBasePath`""
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($UserName) { $arguments += "/N`"$UserName`"" }
|
|
||||||
if ($Password) { $arguments += "/P`"$Password`"" }
|
|
||||||
|
|
||||||
$arguments += "/LoadConfigFromFiles", "`"$ConfigDir`""
|
|
||||||
|
|
||||||
if ($Mode -eq "Full") {
|
|
||||||
Write-Host "Executing full configuration load..."
|
|
||||||
} else {
|
|
||||||
Write-Host "Executing partial configuration load..."
|
|
||||||
|
|
||||||
# Build list file
|
|
||||||
$generatedListFile = $null
|
|
||||||
if ($ListFile) {
|
|
||||||
# Use provided list file
|
|
||||||
if (-not (Test-Path $ListFile)) {
|
|
||||||
Write-Host "Error: list file not found: $ListFile" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
$generatedListFile = $ListFile
|
|
||||||
} else {
|
|
||||||
# Generate from -Files parameter
|
|
||||||
$fileList = $Files -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }
|
|
||||||
$generatedListFile = Join-Path $tempDir "load_list.txt"
|
|
||||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
|
||||||
[System.IO.File]::WriteAllLines($generatedListFile, $fileList, $utf8Bom)
|
|
||||||
|
|
||||||
Write-Host "Files to load: $($fileList.Count)"
|
|
||||||
foreach ($f in $fileList) { Write-Host " $f" }
|
|
||||||
}
|
|
||||||
|
|
||||||
$arguments += "-listFile", "`"$generatedListFile`""
|
|
||||||
$arguments += "-partial"
|
|
||||||
$arguments += "-updateConfigDumpInfo"
|
|
||||||
}
|
|
||||||
|
|
||||||
$arguments += "-Format", $Format
|
|
||||||
|
|
||||||
# --- Extensions ---
|
|
||||||
if ($Extension) {
|
|
||||||
$arguments += "-Extension", "`"$Extension`""
|
|
||||||
} elseif ($AllExtensions) {
|
|
||||||
$arguments += "-AllExtensions"
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- UpdateDB ---
|
|
||||||
if ($UpdateDB) {
|
|
||||||
$arguments += "/UpdateDBCfg"
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Output ---
|
|
||||||
$outFile = Join-Path $tempDir "load_log.txt"
|
|
||||||
$arguments += "/Out", "`"$outFile`""
|
|
||||||
$arguments += "/DisableStartupDialogs"
|
|
||||||
|
|
||||||
# --- Execute ---
|
|
||||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
|
||||||
$exitCode = $process.ExitCode
|
|
||||||
|
|
||||||
# --- Result ---
|
|
||||||
if ($exitCode -eq 0) {
|
|
||||||
Write-Host "Load completed successfully" -ForegroundColor Green
|
|
||||||
} else {
|
|
||||||
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Test-Path $outFile) {
|
|
||||||
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
|
|
||||||
if ($logContent) {
|
|
||||||
Write-Host "--- Log ---"
|
|
||||||
Write-Host $logContent
|
|
||||||
Write-Host "--- End ---"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exit $exitCode
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
if (Test-Path $tempDir) {
|
|
||||||
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# db-load-xml v1.1 — Load 1C configuration from XML files
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import glob
|
|
||||||
import os
|
|
||||||
import random
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
|
||||||
"""Resolve path to 1cv8.exe."""
|
|
||||||
if not v8path:
|
|
||||||
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
|
||||||
if candidates:
|
|
||||||
candidates.sort()
|
|
||||||
return candidates[-1]
|
|
||||||
else:
|
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
elif os.path.isdir(v8path):
|
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
|
||||||
|
|
||||||
if not os.path.isfile(v8path):
|
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
return v8path
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Load 1C configuration from XML files",
|
|
||||||
allow_abbrev=False,
|
|
||||||
)
|
|
||||||
parser.add_argument("-V8Path", default="", help="Path to 1cv8.exe or its bin directory")
|
|
||||||
parser.add_argument("-InfoBasePath", default="", help="Path to file infobase")
|
|
||||||
parser.add_argument("-InfoBaseServer", default="", help="1C server (for server infobase)")
|
|
||||||
parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server")
|
|
||||||
parser.add_argument("-UserName", default="", help="1C user name")
|
|
||||||
parser.add_argument("-Password", default="", help="1C user password")
|
|
||||||
parser.add_argument("-ConfigDir", required=True, help="Directory with XML configuration sources")
|
|
||||||
parser.add_argument(
|
|
||||||
"-Mode",
|
|
||||||
default="Full",
|
|
||||||
choices=["Full", "Partial"],
|
|
||||||
help="Load mode (default: Full)",
|
|
||||||
)
|
|
||||||
parser.add_argument("-Files", default="", help="Comma-separated relative file paths (for Partial mode)")
|
|
||||||
parser.add_argument("-ListFile", default="", help="Path to file list (alternative to -Files, for Partial mode)")
|
|
||||||
parser.add_argument("-Extension", default="", help="Extension name to load")
|
|
||||||
parser.add_argument("-AllExtensions", action="store_true", help="Load all extensions")
|
|
||||||
parser.add_argument(
|
|
||||||
"-Format",
|
|
||||||
default="Hierarchical",
|
|
||||||
choices=["Hierarchical", "Plain"],
|
|
||||||
help="File format (default: Hierarchical)",
|
|
||||||
)
|
|
||||||
parser.add_argument("-UpdateDB", action="store_true", help="Also update database configuration after load")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
|
||||||
|
|
||||||
# --- Validate connection ---
|
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Validate config dir ---
|
|
||||||
if not os.path.exists(args.ConfigDir):
|
|
||||||
print(f"Error: config directory not found: {args.ConfigDir}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Validate Partial mode ---
|
|
||||||
if args.Mode == "Partial" and not args.Files and not args.ListFile:
|
|
||||||
print("Error: -Files or -ListFile required for Partial mode", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_xml_{random.randint(0, 999999)}")
|
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# --- Build arguments ---
|
|
||||||
arguments = ["DESIGNER"]
|
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
|
||||||
arguments += ["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"]
|
|
||||||
else:
|
|
||||||
arguments += ["/F", args.InfoBasePath]
|
|
||||||
|
|
||||||
if args.UserName:
|
|
||||||
arguments.append(f"/N{args.UserName}")
|
|
||||||
if args.Password:
|
|
||||||
arguments.append(f"/P{args.Password}")
|
|
||||||
|
|
||||||
arguments += ["/LoadConfigFromFiles", args.ConfigDir]
|
|
||||||
|
|
||||||
if args.Mode == "Full":
|
|
||||||
print("Executing full configuration load...")
|
|
||||||
else:
|
|
||||||
print("Executing partial configuration load...")
|
|
||||||
|
|
||||||
# Build list file
|
|
||||||
generated_list_file = None
|
|
||||||
if args.ListFile:
|
|
||||||
# Use provided list file
|
|
||||||
if not os.path.isfile(args.ListFile):
|
|
||||||
print(f"Error: list file not found: {args.ListFile}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
generated_list_file = args.ListFile
|
|
||||||
else:
|
|
||||||
# Generate from -Files parameter
|
|
||||||
file_list = [f.strip() for f in args.Files.split(",") if f.strip()]
|
|
||||||
generated_list_file = os.path.join(temp_dir, "load_list.txt")
|
|
||||||
with open(generated_list_file, "w", encoding="utf-8-sig") as f:
|
|
||||||
f.write("\n".join(file_list))
|
|
||||||
|
|
||||||
print(f"Files to load: {len(file_list)}")
|
|
||||||
for fl in file_list:
|
|
||||||
print(f" {fl}")
|
|
||||||
|
|
||||||
arguments += ["-listFile", generated_list_file]
|
|
||||||
arguments.append("-partial")
|
|
||||||
arguments.append("-updateConfigDumpInfo")
|
|
||||||
|
|
||||||
arguments += ["-Format", args.Format]
|
|
||||||
|
|
||||||
# --- Extensions ---
|
|
||||||
if args.Extension:
|
|
||||||
arguments += ["-Extension", args.Extension]
|
|
||||||
elif args.AllExtensions:
|
|
||||||
arguments.append("-AllExtensions")
|
|
||||||
|
|
||||||
# --- UpdateDB ---
|
|
||||||
if args.UpdateDB:
|
|
||||||
arguments.append("/UpdateDBCfg")
|
|
||||||
|
|
||||||
# --- Output ---
|
|
||||||
out_file = os.path.join(temp_dir, "load_log.txt")
|
|
||||||
arguments += ["/Out", out_file]
|
|
||||||
arguments.append("/DisableStartupDialogs")
|
|
||||||
|
|
||||||
# --- Execute ---
|
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
|
||||||
result = subprocess.run(
|
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
|
||||||
|
|
||||||
# --- Result ---
|
|
||||||
if exit_code == 0:
|
|
||||||
print("Load completed successfully")
|
|
||||||
else:
|
|
||||||
print(f"Error loading configuration (code: {exit_code})", file=sys.stderr)
|
|
||||||
|
|
||||||
if os.path.isfile(out_file):
|
|
||||||
try:
|
|
||||||
with open(out_file, "r", encoding="utf-8-sig") as f:
|
|
||||||
log_content = f.read()
|
|
||||||
if log_content:
|
|
||||||
print("--- Log ---")
|
|
||||||
print(log_content)
|
|
||||||
print("--- End ---")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
sys.exit(exit_code)
|
|
||||||
|
|
||||||
finally:
|
|
||||||
if os.path.exists(temp_dir):
|
|
||||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# db-update v1.0 — Update 1C database configuration
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import glob
|
|
||||||
import os
|
|
||||||
import random
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
|
||||||
"""Resolve path to 1cv8.exe."""
|
|
||||||
if not v8path:
|
|
||||||
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
|
|
||||||
if found:
|
|
||||||
return found[-1]
|
|
||||||
else:
|
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
elif os.path.isdir(v8path):
|
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
|
||||||
|
|
||||||
if not os.path.isfile(v8path):
|
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
return v8path
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Update 1C database configuration",
|
|
||||||
allow_abbrev=False,
|
|
||||||
)
|
|
||||||
parser.add_argument("-V8Path", default="")
|
|
||||||
parser.add_argument("-InfoBasePath", default="")
|
|
||||||
parser.add_argument("-InfoBaseServer", default="")
|
|
||||||
parser.add_argument("-InfoBaseRef", default="")
|
|
||||||
parser.add_argument("-UserName", default="")
|
|
||||||
parser.add_argument("-Password", default="")
|
|
||||||
parser.add_argument("-Extension", default="")
|
|
||||||
parser.add_argument("-AllExtensions", action="store_true")
|
|
||||||
parser.add_argument("-Dynamic", default="", choices=["", "+", "-"])
|
|
||||||
parser.add_argument("-Server", action="store_true")
|
|
||||||
parser.add_argument("-WarningsAsErrors", action="store_true")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
|
||||||
|
|
||||||
# --- Validate connection ---
|
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_update_{random.randint(0, 999999)}")
|
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# --- Build arguments ---
|
|
||||||
arguments = ["DESIGNER"]
|
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
|
||||||
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
|
|
||||||
else:
|
|
||||||
arguments.extend(["/F", args.InfoBasePath])
|
|
||||||
|
|
||||||
if args.UserName:
|
|
||||||
arguments.append(f"/N{args.UserName}")
|
|
||||||
if args.Password:
|
|
||||||
arguments.append(f"/P{args.Password}")
|
|
||||||
|
|
||||||
arguments.append("/UpdateDBCfg")
|
|
||||||
|
|
||||||
# --- Options ---
|
|
||||||
if args.Dynamic:
|
|
||||||
arguments.append(f"-Dynamic{args.Dynamic}")
|
|
||||||
if args.Server:
|
|
||||||
arguments.append("-Server")
|
|
||||||
if args.WarningsAsErrors:
|
|
||||||
arguments.append("-WarningsAsErrors")
|
|
||||||
|
|
||||||
# --- Extensions ---
|
|
||||||
if args.Extension:
|
|
||||||
arguments.extend(["-Extension", args.Extension])
|
|
||||||
elif args.AllExtensions:
|
|
||||||
arguments.append("-AllExtensions")
|
|
||||||
|
|
||||||
# --- Output ---
|
|
||||||
out_file = os.path.join(temp_dir, "update_log.txt")
|
|
||||||
arguments.extend(["/Out", out_file])
|
|
||||||
arguments.append("/DisableStartupDialogs")
|
|
||||||
|
|
||||||
# --- Execute ---
|
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
|
||||||
result = subprocess.run(
|
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
|
||||||
|
|
||||||
# --- Result ---
|
|
||||||
if exit_code == 0:
|
|
||||||
print("Database configuration updated successfully")
|
|
||||||
else:
|
|
||||||
print(f"Error updating database configuration (code: {exit_code})", file=sys.stderr)
|
|
||||||
|
|
||||||
if os.path.isfile(out_file):
|
|
||||||
try:
|
|
||||||
with open(out_file, "r", encoding="utf-8-sig") as f:
|
|
||||||
log_content = f.read()
|
|
||||||
if log_content:
|
|
||||||
print("--- Log ---")
|
|
||||||
print(log_content)
|
|
||||||
print("--- End ---")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
sys.exit(exit_code)
|
|
||||||
|
|
||||||
finally:
|
|
||||||
if os.path.isdir(temp_dir):
|
|
||||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
---
|
|
||||||
name: epf-add-form
|
|
||||||
description: Добавить управляемую форму к внешней обработке 1С
|
|
||||||
argument-hint: <ProcessorName> <FormName> [Synonym]
|
|
||||||
allowed-tools:
|
|
||||||
- Bash
|
|
||||||
- Read
|
|
||||||
- Write
|
|
||||||
- Edit
|
|
||||||
- Glob
|
|
||||||
- Grep
|
|
||||||
---
|
|
||||||
|
|
||||||
# /epf-add-form — Добавление формы
|
|
||||||
|
|
||||||
Создаёт управляемую форму и регистрирует её в корневом XML обработки.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```
|
|
||||||
/epf-add-form <ProcessorName> <FormName> [Synonym] [--main]
|
|
||||||
```
|
|
||||||
|
|
||||||
| Параметр | Обязательный | По умолчанию | Описание |
|
|
||||||
|---------------|:------------:|--------------|-------------------------------------------|
|
|
||||||
| ProcessorName | да | — | Имя обработки (должна существовать) |
|
|
||||||
| FormName | да | — | Имя формы |
|
|
||||||
| Synonym | нет | = FormName | Синоним формы |
|
|
||||||
| --main | нет | авто | Установить как форму по умолчанию (автоматически для первой формы) |
|
|
||||||
| SrcDir | нет | `src` | Каталог исходников |
|
|
||||||
|
|
||||||
## Команда
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
powershell.exe -NoProfile -File .claude/skills/epf-add-form/scripts/add-form.ps1 -ProcessorName "<ProcessorName>" -FormName "<FormName>" [-Synonym "<Synonym>"] [-Main] [-SrcDir "<SrcDir>"]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Что создаётся
|
|
||||||
|
|
||||||
```
|
|
||||||
<SrcDir>/<ProcessorName>/Forms/
|
|
||||||
├── <FormName>.xml # Метаданные формы (1 UUID)
|
|
||||||
└── <FormName>/
|
|
||||||
└── Ext/
|
|
||||||
├── Form.xml # Описание формы (logform namespace)
|
|
||||||
└── Form/
|
|
||||||
└── Module.bsl # BSL-модуль с 4 регионами
|
|
||||||
```
|
|
||||||
|
|
||||||
## Что модифицируется
|
|
||||||
|
|
||||||
- `<SrcDir>/<ProcessorName>.xml` — добавляется `<Form>` в `ChildObjects`, обновляется `DefaultForm` (автоматически если это первая форма, или явно при `--main`)
|
|
||||||
|
|
||||||
## Детали
|
|
||||||
|
|
||||||
- FormType: Managed
|
|
||||||
- UsePurposes: PlatformApplication, MobilePlatformApplication
|
|
||||||
- AutoCommandBar с id=-1
|
|
||||||
- Реквизит "Объект" с MainAttribute=true
|
|
||||||
- BSL-модуль содержит 5 регионов: ОбработчикиСобытийФормы, ОбработчикиСобытийЭлементовФормы, ОбработчикиКомандФормы, ОбработчикиОповещений, СлужебныеПроцедурыИФункции
|
|
||||||
@@ -1,207 +0,0 @@
|
|||||||
# epf-add-form v1.0 — Add managed form to 1C processor
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[string]$ProcessorName,
|
|
||||||
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[string]$FormName,
|
|
||||||
|
|
||||||
[string]$Synonym = $FormName,
|
|
||||||
|
|
||||||
[switch]$Main,
|
|
||||||
|
|
||||||
[string]$SrcDir = "src"
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
|
|
||||||
# --- Проверки ---
|
|
||||||
|
|
||||||
$rootXmlPath = Join-Path $SrcDir "$ProcessorName.xml"
|
|
||||||
if (-not (Test-Path $rootXmlPath)) {
|
|
||||||
Write-Error "Корневой файл обработки не найден: $rootXmlPath. Сначала выполните epf-init."
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
$processorDir = Join-Path $SrcDir $ProcessorName
|
|
||||||
$formsDir = Join-Path $processorDir "Forms"
|
|
||||||
$formMetaPath = Join-Path $formsDir "$FormName.xml"
|
|
||||||
|
|
||||||
if (Test-Path $formMetaPath) {
|
|
||||||
Write-Error "Форма уже существует: $formMetaPath"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Создание каталогов ---
|
|
||||||
|
|
||||||
$formDir = Join-Path $formsDir $FormName
|
|
||||||
$formExtDir = Join-Path $formDir "Ext"
|
|
||||||
$formModuleDir = Join-Path $formExtDir "Form"
|
|
||||||
|
|
||||||
New-Item -ItemType Directory -Path $formModuleDir -Force | Out-Null
|
|
||||||
|
|
||||||
# --- Кодировка ---
|
|
||||||
|
|
||||||
$encBom = New-Object System.Text.UTF8Encoding($true)
|
|
||||||
$encNoBom = New-Object System.Text.UTF8Encoding($false)
|
|
||||||
|
|
||||||
# --- 1. Метаданные формы (Forms/<FormName>.xml) ---
|
|
||||||
|
|
||||||
$formUuid = [guid]::NewGuid().ToString()
|
|
||||||
|
|
||||||
$formMetaXml = @"
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<MetaDataObject 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" version="2.17">
|
|
||||||
<Form uuid="$formUuid">
|
|
||||||
<Properties>
|
|
||||||
<Name>$FormName</Name>
|
|
||||||
<Synonym>
|
|
||||||
<v8:item>
|
|
||||||
<v8:lang>ru</v8:lang>
|
|
||||||
<v8:content>$Synonym</v8:content>
|
|
||||||
</v8:item>
|
|
||||||
</Synonym>
|
|
||||||
<Comment/>
|
|
||||||
<FormType>Managed</FormType>
|
|
||||||
<IncludeHelpInContents>false</IncludeHelpInContents>
|
|
||||||
<UsePurposes>
|
|
||||||
<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
|
|
||||||
<v8:Value xsi:type="app:ApplicationUsePurpose">MobilePlatformApplication</v8:Value>
|
|
||||||
</UsePurposes>
|
|
||||||
<ExtendedPresentation/>
|
|
||||||
</Properties>
|
|
||||||
</Form>
|
|
||||||
</MetaDataObject>
|
|
||||||
"@
|
|
||||||
|
|
||||||
[System.IO.File]::WriteAllText($formMetaPath, $formMetaXml, $encBom)
|
|
||||||
|
|
||||||
# --- 2. Описание формы (Forms/<FormName>/Ext/Form.xml) ---
|
|
||||||
|
|
||||||
$formXmlPath = Join-Path $formExtDir "Form.xml"
|
|
||||||
|
|
||||||
$formXml = @"
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Form xmlns="http://v8.1c.ru/8.3/xcf/logform" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" 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: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" version="2.17">
|
|
||||||
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
|
|
||||||
<Autofill>true</Autofill>
|
|
||||||
</AutoCommandBar>
|
|
||||||
<ChildItems/>
|
|
||||||
<Attributes>
|
|
||||||
<Attribute name="Объект" id="1">
|
|
||||||
<Type>
|
|
||||||
<v8:Type>cfg:ExternalDataProcessorObject.$ProcessorName</v8:Type>
|
|
||||||
</Type>
|
|
||||||
<MainAttribute>true</MainAttribute>
|
|
||||||
</Attribute>
|
|
||||||
</Attributes>
|
|
||||||
</Form>
|
|
||||||
"@
|
|
||||||
|
|
||||||
[System.IO.File]::WriteAllText($formXmlPath, $formXml, $encBom)
|
|
||||||
|
|
||||||
# --- 3. BSL-модуль (Forms/<FormName>/Ext/Form/Module.bsl) ---
|
|
||||||
|
|
||||||
$modulePath = Join-Path $formModuleDir "Module.bsl"
|
|
||||||
|
|
||||||
$moduleBsl = @"
|
|
||||||
#Область ОбработчикиСобытийФормы
|
|
||||||
|
|
||||||
#КонецОбласти
|
|
||||||
|
|
||||||
#Область ОбработчикиСобытийЭлементовФормы
|
|
||||||
|
|
||||||
#КонецОбласти
|
|
||||||
|
|
||||||
#Область ОбработчикиКомандФормы
|
|
||||||
|
|
||||||
#КонецОбласти
|
|
||||||
|
|
||||||
#Область ОбработчикиОповещений
|
|
||||||
|
|
||||||
#КонецОбласти
|
|
||||||
|
|
||||||
#Область СлужебныеПроцедурыИФункции
|
|
||||||
|
|
||||||
#КонецОбласти
|
|
||||||
"@
|
|
||||||
|
|
||||||
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $encBom)
|
|
||||||
|
|
||||||
# --- 4. Модификация корневого XML ---
|
|
||||||
|
|
||||||
$rootXmlFull = Resolve-Path $rootXmlPath
|
|
||||||
$xmlDoc = New-Object System.Xml.XmlDocument
|
|
||||||
$xmlDoc.PreserveWhitespace = $true
|
|
||||||
$xmlDoc.Load($rootXmlFull.Path)
|
|
||||||
|
|
||||||
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
|
|
||||||
$nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
|
||||||
|
|
||||||
$childObjects = $xmlDoc.SelectSingleNode("//md:ChildObjects", $nsMgr)
|
|
||||||
if (-not $childObjects) {
|
|
||||||
Write-Error "Не найден элемент ChildObjects в $rootXmlPath"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Добавить <Form> перед первым <Template>, или в конец
|
|
||||||
$formElem = $xmlDoc.CreateElement("Form", "http://v8.1c.ru/8.3/MDClasses")
|
|
||||||
$formElem.InnerText = $FormName
|
|
||||||
|
|
||||||
$firstTemplate = $childObjects.SelectSingleNode("md:Template", $nsMgr)
|
|
||||||
if ($firstTemplate) {
|
|
||||||
# Вставить перед Template, добавив перенос строки + табуляцию
|
|
||||||
$whitespace = $xmlDoc.CreateWhitespace("`n`t`t`t")
|
|
||||||
$childObjects.InsertBefore($whitespace, $firstTemplate) | Out-Null
|
|
||||||
$childObjects.InsertBefore($formElem, $whitespace) | Out-Null
|
|
||||||
} else {
|
|
||||||
# Добавить в конец ChildObjects
|
|
||||||
# Если ChildObjects пустой (самозакрывающийся), нужно добавить форматирование
|
|
||||||
if ($childObjects.ChildNodes.Count -eq 0) {
|
|
||||||
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t`t")) | Out-Null
|
|
||||||
$childObjects.AppendChild($formElem) | Out-Null
|
|
||||||
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t")) | Out-Null
|
|
||||||
} else {
|
|
||||||
$lastChild = $childObjects.LastChild
|
|
||||||
# Вставить перед закрывающим whitespace (если есть), или в конец
|
|
||||||
if ($lastChild.NodeType -eq [System.Xml.XmlNodeType]::Whitespace) {
|
|
||||||
$childObjects.InsertBefore($xmlDoc.CreateWhitespace("`n`t`t`t"), $lastChild) | Out-Null
|
|
||||||
$childObjects.InsertBefore($formElem, $lastChild) | Out-Null
|
|
||||||
} else {
|
|
||||||
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t`t")) | Out-Null
|
|
||||||
$childObjects.AppendChild($formElem) | Out-Null
|
|
||||||
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t")) | Out-Null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Обновить DefaultForm: явно при -Main, или автоматически если это первая форма
|
|
||||||
$existingForms = $childObjects.SelectNodes("md:Form", $nsMgr)
|
|
||||||
$isFirstForm = ($existingForms.Count -eq 1)
|
|
||||||
|
|
||||||
if ($Main -or $isFirstForm) {
|
|
||||||
$defaultForm = $xmlDoc.SelectSingleNode("//md:DefaultForm", $nsMgr)
|
|
||||||
if ($defaultForm) {
|
|
||||||
$defaultForm.InnerText = "ExternalDataProcessor.$ProcessorName.Form.$FormName"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Сохранить с BOM
|
|
||||||
$settings = New-Object System.Xml.XmlWriterSettings
|
|
||||||
$settings.Encoding = $encBom
|
|
||||||
$settings.Indent = $false # Preserve original whitespace
|
|
||||||
|
|
||||||
$stream = New-Object System.IO.FileStream($rootXmlFull.Path, [System.IO.FileMode]::Create)
|
|
||||||
$writer = [System.Xml.XmlWriter]::Create($stream, $settings)
|
|
||||||
$xmlDoc.Save($writer)
|
|
||||||
$writer.Close()
|
|
||||||
$stream.Close()
|
|
||||||
|
|
||||||
Write-Host "[OK] Создана форма: $FormName"
|
|
||||||
Write-Host " Метаданные: $formMetaPath"
|
|
||||||
Write-Host " Описание: $formXmlPath"
|
|
||||||
Write-Host " Модуль: $modulePath"
|
|
||||||
if ($Main -or $isFirstForm) {
|
|
||||||
Write-Host " DefaultForm обновлён"
|
|
||||||
}
|
|
||||||
@@ -1,251 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# add-form v1.0 — Add managed form to 1C external data processor
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
from lxml import etree
|
|
||||||
|
|
||||||
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
|
|
||||||
|
|
||||||
|
|
||||||
def save_xml_with_bom(tree, path):
|
|
||||||
"""Save XML tree to file with UTF-8 BOM."""
|
|
||||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
|
||||||
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"')
|
|
||||||
with open(path, "wb") as f:
|
|
||||||
f.write(b"\xef\xbb\xbf")
|
|
||||||
f.write(xml_bytes)
|
|
||||||
|
|
||||||
|
|
||||||
def write_text_with_bom(path, text):
|
|
||||||
"""Write text to file with UTF-8 BOM."""
|
|
||||||
with open(path, "w", encoding="utf-8-sig") as f:
|
|
||||||
f.write(text)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(description="Add managed form to 1C processor", allow_abbrev=False)
|
|
||||||
parser.add_argument("-ProcessorName", required=True)
|
|
||||||
parser.add_argument("-FormName", required=True)
|
|
||||||
parser.add_argument("-Synonym", default=None)
|
|
||||||
parser.add_argument("-Main", action="store_true")
|
|
||||||
parser.add_argument("-SrcDir", default="src")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
processor_name = args.ProcessorName
|
|
||||||
form_name = args.FormName
|
|
||||||
synonym = args.Synonym if args.Synonym is not None else form_name
|
|
||||||
is_main = args.Main
|
|
||||||
src_dir = args.SrcDir
|
|
||||||
|
|
||||||
# --- Checks ---
|
|
||||||
|
|
||||||
root_xml_path = os.path.join(src_dir, f"{processor_name}.xml")
|
|
||||||
if not os.path.exists(root_xml_path):
|
|
||||||
print(f"Корневой файл обработки не найден: {root_xml_path}. Сначала выполните epf-init.", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
processor_dir = os.path.join(src_dir, processor_name)
|
|
||||||
forms_dir = os.path.join(processor_dir, "Forms")
|
|
||||||
form_meta_path = os.path.join(forms_dir, f"{form_name}.xml")
|
|
||||||
|
|
||||||
if os.path.exists(form_meta_path):
|
|
||||||
print(f"Форма уже существует: {form_meta_path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Create directories ---
|
|
||||||
|
|
||||||
form_dir = os.path.join(forms_dir, form_name)
|
|
||||||
form_ext_dir = os.path.join(form_dir, "Ext")
|
|
||||||
form_module_dir = os.path.join(form_ext_dir, "Form")
|
|
||||||
|
|
||||||
os.makedirs(form_module_dir, exist_ok=True)
|
|
||||||
|
|
||||||
# --- 1. Form metadata (Forms/<FormName>.xml) ---
|
|
||||||
|
|
||||||
form_uuid = str(uuid.uuid4())
|
|
||||||
|
|
||||||
form_meta_xml = (
|
|
||||||
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
|
||||||
'<MetaDataObject 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"'
|
|
||||||
' version="2.17">\n'
|
|
||||||
f'\t<Form uuid="{form_uuid}">\n'
|
|
||||||
'\t\t<Properties>\n'
|
|
||||||
f'\t\t\t<Name>{form_name}</Name>\n'
|
|
||||||
'\t\t\t<Synonym>\n'
|
|
||||||
'\t\t\t\t<v8:item>\n'
|
|
||||||
'\t\t\t\t\t<v8:lang>ru</v8:lang>\n'
|
|
||||||
f'\t\t\t\t\t<v8:content>{synonym}</v8:content>\n'
|
|
||||||
'\t\t\t\t</v8:item>\n'
|
|
||||||
'\t\t\t</Synonym>\n'
|
|
||||||
'\t\t\t<Comment/>\n'
|
|
||||||
'\t\t\t<FormType>Managed</FormType>\n'
|
|
||||||
'\t\t\t<IncludeHelpInContents>false</IncludeHelpInContents>\n'
|
|
||||||
'\t\t\t<UsePurposes>\n'
|
|
||||||
'\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>\n'
|
|
||||||
'\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">MobilePlatformApplication</v8:Value>\n'
|
|
||||||
'\t\t\t</UsePurposes>\n'
|
|
||||||
'\t\t\t<ExtendedPresentation/>\n'
|
|
||||||
'\t\t</Properties>\n'
|
|
||||||
'\t</Form>\n'
|
|
||||||
'</MetaDataObject>'
|
|
||||||
)
|
|
||||||
|
|
||||||
write_text_with_bom(form_meta_path, form_meta_xml)
|
|
||||||
|
|
||||||
# --- 2. Form description (Forms/<FormName>/Ext/Form.xml) ---
|
|
||||||
|
|
||||||
form_xml_path = os.path.join(form_ext_dir, "Form.xml")
|
|
||||||
|
|
||||||
form_xml = (
|
|
||||||
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
|
||||||
'<Form xmlns="http://v8.1c.ru/8.3/xcf/logform"'
|
|
||||||
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
|
|
||||||
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
|
|
||||||
' xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"'
|
|
||||||
' xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"'
|
|
||||||
' 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: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"'
|
|
||||||
' version="2.17">\n'
|
|
||||||
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
|
|
||||||
'\t\t<Autofill>true</Autofill>\n'
|
|
||||||
'\t</AutoCommandBar>\n'
|
|
||||||
'\t<ChildItems/>\n'
|
|
||||||
'\t<Attributes>\n'
|
|
||||||
f'\t\t<Attribute name="\u041e\u0431\u044a\u0435\u043a\u0442" id="1">\n'
|
|
||||||
'\t\t\t<Type>\n'
|
|
||||||
f'\t\t\t\t<v8:Type>cfg:ExternalDataProcessorObject.{processor_name}</v8:Type>\n'
|
|
||||||
'\t\t\t</Type>\n'
|
|
||||||
'\t\t\t<MainAttribute>true</MainAttribute>\n'
|
|
||||||
'\t\t</Attribute>\n'
|
|
||||||
'\t</Attributes>\n'
|
|
||||||
'</Form>'
|
|
||||||
)
|
|
||||||
|
|
||||||
write_text_with_bom(form_xml_path, form_xml)
|
|
||||||
|
|
||||||
# --- 3. BSL module (Forms/<FormName>/Ext/Form/Module.bsl) ---
|
|
||||||
|
|
||||||
module_path = os.path.join(form_module_dir, "Module.bsl")
|
|
||||||
|
|
||||||
module_bsl = (
|
|
||||||
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u0424\u043e\u0440\u043c\u044b\n'
|
|
||||||
'\n'
|
|
||||||
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
|
|
||||||
'\n'
|
|
||||||
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u042d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432\u0424\u043e\u0440\u043c\u044b\n'
|
|
||||||
'\n'
|
|
||||||
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
|
|
||||||
'\n'
|
|
||||||
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u041a\u043e\u043c\u0430\u043d\u0434\u0424\u043e\u0440\u043c\u044b\n'
|
|
||||||
'\n'
|
|
||||||
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
|
|
||||||
'\n'
|
|
||||||
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u041e\u043f\u043e\u0432\u0435\u0449\u0435\u043d\u0438\u0439\n'
|
|
||||||
'\n'
|
|
||||||
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
|
|
||||||
'\n'
|
|
||||||
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u0421\u043b\u0443\u0436\u0435\u0431\u043d\u044b\u0435\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b\u0418\u0424\u0443\u043d\u043a\u0446\u0438\u0438\n'
|
|
||||||
'\n'
|
|
||||||
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438'
|
|
||||||
)
|
|
||||||
|
|
||||||
write_text_with_bom(module_path, module_bsl)
|
|
||||||
|
|
||||||
# --- 4. Modify root XML ---
|
|
||||||
|
|
||||||
root_xml_full = os.path.abspath(root_xml_path)
|
|
||||||
parser_xml = etree.XMLParser(remove_blank_text=False)
|
|
||||||
tree = etree.parse(root_xml_full, parser_xml)
|
|
||||||
root = tree.getroot()
|
|
||||||
|
|
||||||
ns = "http://v8.1c.ru/8.3/MDClasses"
|
|
||||||
child_objects = root.find(".//md:ChildObjects", NSMAP)
|
|
||||||
if child_objects is None:
|
|
||||||
print(f"Не найден элемент ChildObjects в {root_xml_path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Add <Form> before first <Template>, or at end
|
|
||||||
form_elem = etree.Element(f"{{{ns}}}Form")
|
|
||||||
form_elem.text = form_name
|
|
||||||
|
|
||||||
first_template = child_objects.find("md:Template", NSMAP)
|
|
||||||
if first_template is not None:
|
|
||||||
# Insert before Template, adding newline + indent
|
|
||||||
idx = list(child_objects).index(first_template)
|
|
||||||
child_objects.insert(idx, form_elem)
|
|
||||||
# Set whitespace: form_elem gets same tail pattern
|
|
||||||
form_elem.tail = "\n\t\t\t"
|
|
||||||
else:
|
|
||||||
# Add to end of ChildObjects
|
|
||||||
children = list(child_objects)
|
|
||||||
if len(children) == 0 and (child_objects.text is None or child_objects.text.strip() == ""):
|
|
||||||
# Empty ChildObjects (self-closing)
|
|
||||||
child_objects.text = "\n\t\t\t"
|
|
||||||
child_objects.append(form_elem)
|
|
||||||
form_elem.tail = "\n\t\t"
|
|
||||||
else:
|
|
||||||
if len(children) > 0:
|
|
||||||
last_child = children[-1]
|
|
||||||
old_tail = last_child.tail
|
|
||||||
last_child.tail = "\n\t\t\t"
|
|
||||||
child_objects.append(form_elem)
|
|
||||||
form_elem.tail = old_tail if old_tail else "\n\t\t"
|
|
||||||
else:
|
|
||||||
child_objects.text = (child_objects.text or "") + "\n\t\t\t"
|
|
||||||
child_objects.append(form_elem)
|
|
||||||
form_elem.tail = "\n\t\t"
|
|
||||||
|
|
||||||
# Update DefaultForm: explicitly with -Main, or automatically if this is the first form
|
|
||||||
existing_forms = child_objects.findall("md:Form", NSMAP)
|
|
||||||
is_first_form = len(existing_forms) == 1
|
|
||||||
|
|
||||||
if is_main or is_first_form:
|
|
||||||
default_form = root.find(".//md:DefaultForm", NSMAP)
|
|
||||||
if default_form is not None:
|
|
||||||
default_form.text = f"ExternalDataProcessor.{processor_name}.Form.{form_name}"
|
|
||||||
|
|
||||||
# Save with BOM
|
|
||||||
save_xml_with_bom(tree, root_xml_full)
|
|
||||||
|
|
||||||
print(f"[OK] Создана форма: {form_name}")
|
|
||||||
print(f" Метаданные: {form_meta_path}")
|
|
||||||
print(f" Описание: {form_xml_path}")
|
|
||||||
print(f" Модуль: {module_path}")
|
|
||||||
if is_main or is_first_form:
|
|
||||||
print(" DefaultForm обновлён")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# epf-dump v1.0 — Dump external data processor or report (EPF/ERF) to XML sources
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import glob
|
|
||||||
import os
|
|
||||||
import random
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
|
||||||
"""Resolve path to 1cv8.exe."""
|
|
||||||
if not v8path:
|
|
||||||
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
|
||||||
if candidates:
|
|
||||||
candidates.sort()
|
|
||||||
return candidates[-1]
|
|
||||||
else:
|
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
elif os.path.isdir(v8path):
|
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
|
||||||
|
|
||||||
if not os.path.isfile(v8path):
|
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
return v8path
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Dump external data processor or report (EPF/ERF) to XML sources",
|
|
||||||
allow_abbrev=False,
|
|
||||||
)
|
|
||||||
parser.add_argument("-V8Path", default="", help="Path to 1cv8.exe or its bin directory")
|
|
||||||
parser.add_argument("-InfoBasePath", default="", help="Path to file infobase")
|
|
||||||
parser.add_argument("-InfoBaseServer", default="", help="1C server (for server infobase)")
|
|
||||||
parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server")
|
|
||||||
parser.add_argument("-UserName", default="", help="1C user name")
|
|
||||||
parser.add_argument("-Password", default="", help="1C user password")
|
|
||||||
parser.add_argument("-InputFile", required=True, help="Path to EPF/ERF file")
|
|
||||||
parser.add_argument("-OutputDir", required=True, help="Directory for dumped XML sources")
|
|
||||||
parser.add_argument(
|
|
||||||
"-Format",
|
|
||||||
default="Hierarchical",
|
|
||||||
choices=["Hierarchical", "Plain"],
|
|
||||||
help="Dump format (default: Hierarchical)",
|
|
||||||
)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
|
||||||
|
|
||||||
# --- Validate database connection ---
|
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
|
||||||
print("Error: database connection required. Specify -InfoBasePath or -InfoBaseServer/-InfoBaseRef", file=sys.stderr)
|
|
||||||
print("Dump in an empty database loses reference types (CatalogRef, DocumentRef, etc.) irreversibly.")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Validate input file ---
|
|
||||||
if not os.path.isfile(args.InputFile):
|
|
||||||
print(f"Error: input file not found: {args.InputFile}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Ensure output directory exists ---
|
|
||||||
if not os.path.exists(args.OutputDir):
|
|
||||||
os.makedirs(args.OutputDir, exist_ok=True)
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"epf_dump_{random.randint(0, 999999)}")
|
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# --- Build arguments ---
|
|
||||||
arguments = ["DESIGNER"]
|
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
|
||||||
arguments += ["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"]
|
|
||||||
else:
|
|
||||||
arguments += ["/F", args.InfoBasePath]
|
|
||||||
|
|
||||||
if args.UserName:
|
|
||||||
arguments.append(f"/N{args.UserName}")
|
|
||||||
if args.Password:
|
|
||||||
arguments.append(f"/P{args.Password}")
|
|
||||||
|
|
||||||
arguments += ["/DumpExternalDataProcessorOrReportToFiles", args.OutputDir, args.InputFile]
|
|
||||||
arguments += ["-Format", args.Format]
|
|
||||||
|
|
||||||
# --- Output ---
|
|
||||||
out_file = os.path.join(temp_dir, "dump_log.txt")
|
|
||||||
arguments += ["/Out", out_file]
|
|
||||||
arguments.append("/DisableStartupDialogs")
|
|
||||||
|
|
||||||
# --- Execute ---
|
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
|
||||||
result = subprocess.run(
|
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
|
||||||
|
|
||||||
# --- Result ---
|
|
||||||
if exit_code == 0:
|
|
||||||
print(f"Dump completed successfully to: {args.OutputDir}")
|
|
||||||
else:
|
|
||||||
print(f"Error dumping (code: {exit_code})", file=sys.stderr)
|
|
||||||
|
|
||||||
if os.path.isfile(out_file):
|
|
||||||
try:
|
|
||||||
with open(out_file, "r", encoding="utf-8-sig") as f:
|
|
||||||
log_content = f.read()
|
|
||||||
if log_content:
|
|
||||||
print("--- Log ---")
|
|
||||||
print(log_content)
|
|
||||||
print("--- End ---")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
sys.exit(exit_code)
|
|
||||||
|
|
||||||
finally:
|
|
||||||
if os.path.exists(temp_dir):
|
|
||||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
---
|
|
||||||
name: epf-validate
|
|
||||||
description: Валидация внешней обработки 1С (EPF). Используй после создания или модификации обработки для проверки корректности
|
|
||||||
argument-hint: <ObjectPath> [-Detailed] [-MaxErrors 30]
|
|
||||||
allowed-tools:
|
|
||||||
- Bash
|
|
||||||
- Read
|
|
||||||
- Glob
|
|
||||||
---
|
|
||||||
|
|
||||||
# /epf-validate — валидация внешней обработки (EPF)
|
|
||||||
|
|
||||||
Проверяет структурную корректность XML-исходников внешней обработки: корневую структуру, InternalInfo, свойства, ChildObjects, реквизиты, табличные части, уникальность имён, наличие файлов форм и макетов. Также работает для внешних отчётов (ERF).
|
|
||||||
|
|
||||||
## Параметры
|
|
||||||
|
|
||||||
| Параметр | Обяз. | Умолч. | Описание |
|
|
||||||
|------------|:-----:|---------|-------------------------------------------------|
|
|
||||||
| ObjectPath | да | — | Путь к корневому XML или каталогу обработки |
|
|
||||||
| Detailed | нет | — | Показывать [OK] для каждой проверки |
|
|
||||||
| MaxErrors | нет | 30 | Остановиться после N ошибок |
|
|
||||||
| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) |
|
|
||||||
|
|
||||||
## Команда
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
powershell.exe -NoProfile -File .claude/skills/epf-validate/scripts/epf-validate.ps1 -ObjectPath "src/МояОбработка"
|
|
||||||
powershell.exe -NoProfile -File .claude/skills/epf-validate/scripts/epf-validate.ps1 -ObjectPath "src/МояОбработка/МояОбработка.xml"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Проверки
|
|
||||||
|
|
||||||
| # | Проверка | Серьёзность |
|
|
||||||
|----|-------------------------------------------------------|--------------|
|
|
||||||
| 1 | Root structure: MetaDataObject/ExternalDataProcessor | ERROR |
|
|
||||||
| 2 | InternalInfo: ClassId, ContainedObject, GeneratedType | ERROR / WARN |
|
|
||||||
| 3 | Properties: Name (identifier), Synonym | ERROR / WARN |
|
|
||||||
| 4 | ChildObjects: допустимые типы, порядок | ERROR / WARN |
|
|
||||||
| 5 | Cross-references: DefaultForm → Form, AuxiliaryForm | ERROR / WARN |
|
|
||||||
| 6 | Attributes: UUID, Name, Type | ERROR |
|
|
||||||
| 7 | TabularSections: UUID, Name, GeneratedType, Attributes | ERROR / WARN |
|
|
||||||
| 8 | Уникальность имён (Attribute, TS, Form, Template, Command) | ERROR |
|
|
||||||
| 9 | Файлы: формы (.xml + Ext/Form.xml), макеты | ERROR |
|
|
||||||
| 10 | Дескрипторы форм: корневая структура, uuid, Name, FormType | ERROR / WARN |
|
|
||||||
|
|
||||||
Exit code: 0 = OK, 1 = есть ошибки. По умолчанию краткий вывод. `-Detailed` для поштучной детализации.
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
---
|
|
||||||
name: erf-validate
|
|
||||||
description: Валидация внешнего отчёта 1С (ERF). Используй после создания или модификации отчёта для проверки корректности
|
|
||||||
argument-hint: <ObjectPath> [-Detailed] [-MaxErrors 30]
|
|
||||||
allowed-tools:
|
|
||||||
- Bash
|
|
||||||
- Read
|
|
||||||
- Glob
|
|
||||||
---
|
|
||||||
|
|
||||||
# /erf-validate — валидация внешнего отчёта (ERF)
|
|
||||||
|
|
||||||
Проверяет структурную корректность XML-исходников внешнего отчёта: корневую структуру, InternalInfo, свойства (включая MainDataCompositionSchema), ChildObjects, реквизиты, табличные части, уникальность имён, наличие файлов форм и макетов.
|
|
||||||
|
|
||||||
Использует тот же скрипт, что и `/epf-validate` — автоопределение по типу элемента (ExternalReport).
|
|
||||||
|
|
||||||
## Параметры
|
|
||||||
|
|
||||||
| Параметр | Обяз. | Умолч. | Описание |
|
|
||||||
|------------|:-----:|---------|-------------------------------------------------|
|
|
||||||
| ObjectPath | да | — | Путь к корневому XML или каталогу отчёта |
|
|
||||||
| Detailed | нет | — | Показывать [OK] для каждой проверки |
|
|
||||||
| MaxErrors | нет | 30 | Остановиться после N ошибок |
|
|
||||||
| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) |
|
|
||||||
|
|
||||||
## Команда
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
powershell.exe -NoProfile -File .claude/skills/epf-validate/scripts/epf-validate.ps1 -ObjectPath "src/МойОтчёт"
|
|
||||||
powershell.exe -NoProfile -File .claude/skills/epf-validate/scripts/epf-validate.ps1 -ObjectPath "src/МойОтчёт/МойОтчёт.xml"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Проверки
|
|
||||||
|
|
||||||
| # | Проверка | Серьёзность |
|
|
||||||
|----|-------------------------------------------------------|--------------|
|
|
||||||
| 1 | Root structure: MetaDataObject/ExternalReport | ERROR |
|
|
||||||
| 2 | InternalInfo: ClassId, ContainedObject, GeneratedType | ERROR / WARN |
|
|
||||||
| 3 | Properties: Name, Synonym, MainDataCompositionSchema | ERROR / WARN |
|
|
||||||
| 4 | ChildObjects: допустимые типы, порядок | ERROR / WARN |
|
|
||||||
| 5 | Cross-references: DefaultForm, MainDCS → Template | ERROR / WARN |
|
|
||||||
| 6 | Attributes: UUID, Name, Type | ERROR |
|
|
||||||
| 7 | TabularSections: UUID, Name, GeneratedType, Attributes | ERROR / WARN |
|
|
||||||
| 8 | Уникальность имён (Attribute, TS, Form, Template, Command) | ERROR |
|
|
||||||
| 9 | Файлы: формы (.xml + Ext/Form.xml), макеты | ERROR |
|
|
||||||
| 10 | Дескрипторы форм: корневая структура, uuid, Name, FormType | ERROR / WARN |
|
|
||||||
|
|
||||||
Exit code: 0 = OK, 1 = есть ошибки. По умолчанию краткий вывод. `-Detailed` для поштучной детализации.
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,50 +0,0 @@
|
|||||||
---
|
|
||||||
name: form-validate
|
|
||||||
description: Валидация управляемой формы 1С. Используй после создания или модификации формы для проверки корректности. При наличии BaseForm автоматически проверяет callType и ID расширений
|
|
||||||
argument-hint: <FormPath> [-Detailed] [-MaxErrors 30]
|
|
||||||
allowed-tools:
|
|
||||||
- Bash
|
|
||||||
- Read
|
|
||||||
- Glob
|
|
||||||
---
|
|
||||||
|
|
||||||
# /form-validate — валидация управляемой формы 1С
|
|
||||||
|
|
||||||
Проверяет Form.xml на структурные ошибки: уникальность ID, наличие companion-элементов, корректность ссылок DataPath и команд.
|
|
||||||
|
|
||||||
## Параметры
|
|
||||||
|
|
||||||
| Параметр | Обяз. | Умолч. | Описание |
|
|
||||||
|-----------|:-----:|---------|-----------------------------------------|
|
|
||||||
| FormPath | да | — | Путь к файлу Form.xml |
|
|
||||||
| Detailed | нет | — | Показывать [OK] для каждой проверки |
|
|
||||||
| MaxErrors | нет | 30 | Остановиться после N ошибок |
|
|
||||||
|
|
||||||
## Команда
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
powershell.exe -NoProfile -File .claude/skills/form-validate/scripts/form-validate.ps1 -FormPath "Catalogs/Номенклатура/Forms/ФормаЭлемента"
|
|
||||||
powershell.exe -NoProfile -File .claude/skills/form-validate/scripts/form-validate.ps1 -FormPath "src/МояОбработка/Forms/Форма/Ext/Form.xml"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Проверки
|
|
||||||
|
|
||||||
| # | Проверка | Серьёзность |
|
|
||||||
|---|---|---|
|
|
||||||
| 1 | Корневой элемент `<Form>`, version="2.17" | ERROR / WARN |
|
|
||||||
| 2 | `<AutoCommandBar>` присутствует, id="-1" | ERROR |
|
|
||||||
| 3 | Уникальность ID элементов (отдельный пул) | ERROR |
|
|
||||||
| 4 | Уникальность ID реквизитов (отдельный пул) | ERROR |
|
|
||||||
| 5 | Уникальность ID команд (отдельный пул) | ERROR |
|
|
||||||
| 6 | Companion-элементы (ContextMenu, ExtendedTooltip, и др.) | ERROR |
|
|
||||||
| 7 | DataPath → ссылается на существующий реквизит | ERROR |
|
|
||||||
| 8 | CommandName кнопок → ссылается на существующую команду | ERROR |
|
|
||||||
| 9 | События имеют непустые имена обработчиков | ERROR |
|
|
||||||
| 10 | Команды имеют Action (обработчик) | ERROR |
|
|
||||||
| 11 | Не более одного MainAttribute | ERROR |
|
|
||||||
| 12 | BaseForm: наличие и version (при расширении) | OK / WARN |
|
|
||||||
| 13 | callType значения: Before, After, Override | ERROR |
|
|
||||||
| 14 | ID расширения >= 1000000 для добавленных attrs/commands | WARN |
|
|
||||||
| 15 | callType без BaseForm — некорректная структура | WARN |
|
|
||||||
|
|
||||||
Exit code: 0 = OK, 1 = есть ошибки. По умолчанию краткий вывод. `-Detailed` для поштучной детализации.
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
# help-add v1.2 — Add built-in help to 1C object
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[string]$ObjectName,
|
|
||||||
|
|
||||||
[string]$Lang = "ru",
|
|
||||||
|
|
||||||
[string]$SrcDir = "src"
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
|
|
||||||
# --- Проверки ---
|
|
||||||
|
|
||||||
$objectDir = Join-Path $SrcDir $ObjectName
|
|
||||||
$extDir = Join-Path $objectDir "Ext"
|
|
||||||
|
|
||||||
if (-not (Test-Path $extDir)) {
|
|
||||||
Write-Error "Каталог объекта не найден: $extDir. Проверьте путь ObjectName (например Catalogs/МойСправочник)."
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
$helpXmlPath = Join-Path $extDir "Help.xml"
|
|
||||||
if (Test-Path $helpXmlPath) {
|
|
||||||
Write-Error "Справка уже существует: $helpXmlPath"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Кодировка ---
|
|
||||||
|
|
||||||
$encBom = New-Object System.Text.UTF8Encoding($true)
|
|
||||||
|
|
||||||
# --- 1. Help.xml ---
|
|
||||||
|
|
||||||
$helpXml = @"
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Help xmlns="http://v8.1c.ru/8.3/xcf/extrnprops" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
|
||||||
<Page>$Lang</Page>
|
|
||||||
</Help>
|
|
||||||
"@
|
|
||||||
|
|
||||||
[System.IO.File]::WriteAllText($helpXmlPath, $helpXml, $encBom)
|
|
||||||
|
|
||||||
# --- 2. Help/<lang>.html ---
|
|
||||||
|
|
||||||
$helpDir = Join-Path $extDir "Help"
|
|
||||||
New-Item -ItemType Directory -Path $helpDir -Force | Out-Null
|
|
||||||
|
|
||||||
$helpHtmlPath = Join-Path $helpDir "$Lang.html"
|
|
||||||
|
|
||||||
$helpHtml = @"
|
|
||||||
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
|
||||||
<link rel="stylesheet" type="text/css" href="v8help://service_book/service_style"/>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>$ObjectName</h1>
|
|
||||||
<p>Описание.</p>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
"@
|
|
||||||
|
|
||||||
[System.IO.File]::WriteAllText($helpHtmlPath, $helpHtml, $encBom)
|
|
||||||
|
|
||||||
# --- 3. Проверка IncludeHelpInContents в метаданных форм ---
|
|
||||||
|
|
||||||
$formsDir = Join-Path $objectDir "Forms"
|
|
||||||
if (Test-Path $formsDir) {
|
|
||||||
$formMetaFiles = Get-ChildItem -Path $formsDir -Filter "*.xml" -File
|
|
||||||
foreach ($formMeta in $formMetaFiles) {
|
|
||||||
$xmlDoc = New-Object System.Xml.XmlDocument
|
|
||||||
$xmlDoc.PreserveWhitespace = $true
|
|
||||||
$xmlDoc.Load($formMeta.FullName)
|
|
||||||
|
|
||||||
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
|
|
||||||
$nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
|
||||||
|
|
||||||
$includeHelp = $xmlDoc.SelectSingleNode("//md:IncludeHelpInContents", $nsMgr)
|
|
||||||
if (-not $includeHelp) {
|
|
||||||
# Добавить после <FormType>
|
|
||||||
$formType = $xmlDoc.SelectSingleNode("//md:FormType", $nsMgr)
|
|
||||||
if ($formType) {
|
|
||||||
$newElem = $xmlDoc.CreateElement("IncludeHelpInContents", "http://v8.1c.ru/8.3/MDClasses")
|
|
||||||
$newElem.InnerText = "false"
|
|
||||||
$parent = $formType.ParentNode
|
|
||||||
$nextSibling = $formType.NextSibling
|
|
||||||
# Вставить перенос + табуляцию + элемент
|
|
||||||
$ws = $xmlDoc.CreateWhitespace("`n`t`t`t")
|
|
||||||
if ($nextSibling) {
|
|
||||||
$parent.InsertBefore($ws, $nextSibling) | Out-Null
|
|
||||||
$parent.InsertBefore($newElem, $ws) | Out-Null
|
|
||||||
} else {
|
|
||||||
$parent.AppendChild($ws) | Out-Null
|
|
||||||
$parent.AppendChild($newElem) | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
$settings = New-Object System.Xml.XmlWriterSettings
|
|
||||||
$settings.Encoding = $encBom
|
|
||||||
$settings.Indent = $false
|
|
||||||
$stream = New-Object System.IO.FileStream($formMeta.FullName, [System.IO.FileMode]::Create)
|
|
||||||
$writer = [System.Xml.XmlWriter]::Create($stream, $settings)
|
|
||||||
$xmlDoc.Save($writer)
|
|
||||||
$writer.Close()
|
|
||||||
$stream.Close()
|
|
||||||
|
|
||||||
Write-Host " IncludeHelpInContents добавлен: $($formMeta.Name)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "[OK] Создана справка: $ObjectName"
|
|
||||||
Write-Host " Метаданные: $helpXmlPath"
|
|
||||||
Write-Host " Страница: $helpHtmlPath"
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# add-help v1.2 — Add built-in help to 1C object
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from lxml import etree
|
|
||||||
|
|
||||||
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
|
|
||||||
|
|
||||||
|
|
||||||
def save_xml_with_bom(tree, path):
|
|
||||||
"""Save XML tree to file with UTF-8 BOM."""
|
|
||||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
|
||||||
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"')
|
|
||||||
with open(path, "wb") as f:
|
|
||||||
f.write(b"\xef\xbb\xbf")
|
|
||||||
f.write(xml_bytes)
|
|
||||||
|
|
||||||
|
|
||||||
def write_text_with_bom(path, text):
|
|
||||||
"""Write text to file with UTF-8 BOM."""
|
|
||||||
with open(path, "w", encoding="utf-8-sig") as f:
|
|
||||||
f.write(text)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(description="Add built-in help to 1C object", allow_abbrev=False)
|
|
||||||
parser.add_argument("-ObjectName", required=True)
|
|
||||||
parser.add_argument("-Lang", default="ru")
|
|
||||||
parser.add_argument("-SrcDir", default="src")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
object_name = args.ObjectName
|
|
||||||
lang = args.Lang
|
|
||||||
src_dir = args.SrcDir
|
|
||||||
|
|
||||||
# --- Checks ---
|
|
||||||
|
|
||||||
object_dir = os.path.join(src_dir, object_name)
|
|
||||||
ext_dir = os.path.join(object_dir, "Ext")
|
|
||||||
|
|
||||||
if not os.path.isdir(ext_dir):
|
|
||||||
print(f"Каталог объекта не найден: {ext_dir}. Проверьте путь ObjectName (например Catalogs/МойСправочник).", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
help_xml_path = os.path.join(ext_dir, "Help.xml")
|
|
||||||
if os.path.exists(help_xml_path):
|
|
||||||
print(f"Справка уже существует: {help_xml_path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- 1. Help.xml ---
|
|
||||||
|
|
||||||
help_xml = (
|
|
||||||
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
|
||||||
'<Help xmlns="http://v8.1c.ru/8.3/xcf/extrnprops"'
|
|
||||||
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
|
|
||||||
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
|
||||||
' version="2.17">\n'
|
|
||||||
f'\t<Page>{lang}</Page>\n'
|
|
||||||
'</Help>'
|
|
||||||
)
|
|
||||||
|
|
||||||
write_text_with_bom(help_xml_path, help_xml)
|
|
||||||
|
|
||||||
# --- 2. Help/<lang>.html ---
|
|
||||||
|
|
||||||
help_dir = os.path.join(ext_dir, "Help")
|
|
||||||
os.makedirs(help_dir, exist_ok=True)
|
|
||||||
|
|
||||||
help_html_path = os.path.join(help_dir, f"{lang}.html")
|
|
||||||
|
|
||||||
help_html = (
|
|
||||||
'<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">\n'
|
|
||||||
'<html>\n'
|
|
||||||
'<head>\n'
|
|
||||||
' <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>\n'
|
|
||||||
' <link rel="stylesheet" type="text/css" href="v8help://service_book/service_style"/>\n'
|
|
||||||
'</head>\n'
|
|
||||||
'<body>\n'
|
|
||||||
f' <h1>{object_name}</h1>\n'
|
|
||||||
' <p>Описание.</p>\n'
|
|
||||||
'</body>\n'
|
|
||||||
'</html>'
|
|
||||||
)
|
|
||||||
|
|
||||||
write_text_with_bom(help_html_path, help_html)
|
|
||||||
|
|
||||||
# --- 3. Check IncludeHelpInContents in form metadata ---
|
|
||||||
|
|
||||||
forms_dir = os.path.join(object_dir, "Forms")
|
|
||||||
if os.path.isdir(forms_dir):
|
|
||||||
for entry in os.listdir(forms_dir):
|
|
||||||
if not entry.endswith(".xml"):
|
|
||||||
continue
|
|
||||||
form_meta_full = os.path.join(forms_dir, entry)
|
|
||||||
if not os.path.isfile(form_meta_full):
|
|
||||||
continue
|
|
||||||
|
|
||||||
parser_xml = etree.XMLParser(remove_blank_text=False)
|
|
||||||
form_tree = etree.parse(form_meta_full, parser_xml)
|
|
||||||
form_root = form_tree.getroot()
|
|
||||||
|
|
||||||
include_help = form_root.find(".//md:IncludeHelpInContents", NSMAP)
|
|
||||||
if include_help is not None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Add after <FormType>
|
|
||||||
form_type = form_root.find(".//md:FormType", NSMAP)
|
|
||||||
if form_type is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
parent = form_type.getparent()
|
|
||||||
ns = "http://v8.1c.ru/8.3/MDClasses"
|
|
||||||
new_elem = etree.SubElement(parent, f"{{{ns}}}IncludeHelpInContents")
|
|
||||||
new_elem.text = "false"
|
|
||||||
# Remove SubElement's auto-placement (it appends to end) and insert after FormType
|
|
||||||
parent.remove(new_elem)
|
|
||||||
|
|
||||||
# Find index of FormType in parent
|
|
||||||
form_type_idx = list(parent).index(form_type)
|
|
||||||
|
|
||||||
# Insert after FormType
|
|
||||||
parent.insert(form_type_idx + 1, new_elem)
|
|
||||||
|
|
||||||
# Whitespace handling: copy FormType's tail as new_elem's tail,
|
|
||||||
# and set FormType's tail to include newline + indent
|
|
||||||
new_elem.tail = form_type.tail
|
|
||||||
form_type.tail = "\n\t\t\t"
|
|
||||||
|
|
||||||
save_xml_with_bom(form_tree, form_meta_full)
|
|
||||||
|
|
||||||
print(f" IncludeHelpInContents добавлен: {entry}")
|
|
||||||
|
|
||||||
print(f"[OK] Создана справка: {object_name}")
|
|
||||||
print(f" Метаданные: {help_xml_path}")
|
|
||||||
print(f" Страница: {help_html_path}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
---
|
|
||||||
name: interface-edit
|
|
||||||
description: Настройка командного интерфейса подсистемы 1С. Используй когда нужно скрыть или показать команды, разместить в группах, настроить порядок
|
|
||||||
argument-hint: <CIPath> <Operation> <Value>
|
|
||||||
allowed-tools:
|
|
||||||
- Bash
|
|
||||||
- Read
|
|
||||||
- Write
|
|
||||||
- Glob
|
|
||||||
---
|
|
||||||
|
|
||||||
# /interface-edit — редактирование CommandInterface.xml
|
|
||||||
|
|
||||||
Операции: hide, show, place, order, subsystem-order, group-order. Подробнее: `.claude/skills/interface-edit/reference.md`
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
powershell.exe -NoProfile -File '.claude/skills/interface-edit/scripts/interface-edit.ps1' -CIPath '<path>' -Operation hide -Value '<cmd>'
|
|
||||||
```
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
---
|
|
||||||
name: interface-validate
|
|
||||||
description: Валидация командного интерфейса 1С. Используй после настройки командного интерфейса подсистемы для проверки корректности
|
|
||||||
argument-hint: <CIPath> [-Detailed] [-MaxErrors 30]
|
|
||||||
allowed-tools:
|
|
||||||
- Bash
|
|
||||||
- Read
|
|
||||||
- Glob
|
|
||||||
---
|
|
||||||
|
|
||||||
# /interface-validate — валидация CommandInterface.xml
|
|
||||||
|
|
||||||
Проверяет XML командного интерфейса на структурные ошибки: корневой элемент, допустимые секции, порядок, формат ссылок на команды, дубликаты.
|
|
||||||
|
|
||||||
## Параметры
|
|
||||||
|
|
||||||
| Параметр | Обяз. | Умолч. | Описание |
|
|
||||||
|-----------|:-----:|---------|-----------------------------------------|
|
|
||||||
| CIPath | да | — | Путь к CommandInterface.xml |
|
|
||||||
| Detailed | нет | — | Показывать [OK] для каждой проверки |
|
|
||||||
| MaxErrors | нет | 30 | Остановиться после N ошибок |
|
|
||||||
| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) |
|
|
||||||
|
|
||||||
## Команда
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
powershell.exe -NoProfile -File ".claude/skills/interface-validate/scripts/interface-validate.ps1" -CIPath "Subsystems/Продажи"
|
|
||||||
powershell.exe -NoProfile -File ".claude/skills/interface-validate/scripts/interface-validate.ps1" -CIPath "Subsystems/Продажи/Ext/CommandInterface.xml"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Проверки (13)
|
|
||||||
|
|
||||||
| # | Проверка | Серьёзность |
|
|
||||||
|----|--------------------------------------------------------------|-------------|
|
|
||||||
| 1 | XML well-formedness + root element (CommandInterface, version, namespace) | ERROR |
|
|
||||||
| 2 | Допустимые дочерние элементы (только 5 секций) | ERROR |
|
|
||||||
| 3 | Порядок секций корректен | ERROR |
|
|
||||||
| 4 | Нет дублирующихся секций | ERROR |
|
|
||||||
| 5 | CommandsVisibility — Command.name + Visibility/xr:Common | ERROR |
|
|
||||||
| 6 | CommandsVisibility — нет дубликатов по name | WARN |
|
|
||||||
| 7 | CommandsPlacement — Command.name + CommandGroup + Placement | ERROR |
|
|
||||||
| 8 | CommandsOrder — Command.name + CommandGroup | ERROR |
|
|
||||||
| 9 | SubsystemsOrder — Subsystem непустой, формат Subsystem.X | ERROR |
|
|
||||||
| 10 | SubsystemsOrder — нет дубликатов | WARN |
|
|
||||||
| 11 | GroupsOrder — Group непустой | ERROR |
|
|
||||||
| 12 | GroupsOrder — нет дубликатов | WARN |
|
|
||||||
| 13 | Формат ссылок на команды | WARN |
|
|
||||||
|
|
||||||
Exit code: 0 = OK, 1 = есть ошибки. По умолчанию краткий вывод. `-Detailed` для поштучной детализации.
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
---
|
|
||||||
name: meta-remove
|
|
||||||
description: Удалить объект метаданных из конфигурации 1С. Используй когда пользователь просит удалить, убрать объект из конфигурации
|
|
||||||
argument-hint: <ConfigDir> -Object <Type.Name>
|
|
||||||
allowed-tools:
|
|
||||||
- Bash
|
|
||||||
- Read
|
|
||||||
- Glob
|
|
||||||
- AskUserQuestion
|
|
||||||
---
|
|
||||||
|
|
||||||
# /meta-remove — удаление объекта метаданных
|
|
||||||
|
|
||||||
Безопасно удаляет объект из XML-выгрузки конфигурации. Перед удалением проверяет ссылки на объект в реквизитах, коде и других метаданных. Если ссылки найдены — удаление блокируется.
|
|
||||||
|
|
||||||
## Использование
|
|
||||||
|
|
||||||
```
|
|
||||||
/meta-remove <ConfigDir> -Object <Type.Name>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Параметры
|
|
||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
|
||||||
|------------|:------------:|-------------------------------------------------|
|
|
||||||
| ConfigDir | да | Корневая директория выгрузки (где Configuration.xml) |
|
|
||||||
| Object | да | Тип и имя объекта: `Catalog.Товары`, `Document.Заказ` и т.д. |
|
|
||||||
| DryRun | нет | Только показать что будет удалено, без изменений |
|
|
||||||
| KeepFiles | нет | Не удалять файлы, только дерегистрировать |
|
|
||||||
| Force | нет | Удалить несмотря на найденные ссылки |
|
|
||||||
|
|
||||||
## Команда
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
powershell.exe -NoProfile -File .claude/skills/meta-remove/scripts/meta-remove.ps1 -ConfigDir "<путь>" -Object "Catalog.Товары"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Что делает
|
|
||||||
|
|
||||||
1. **Находит файлы объекта**: `{TypePlural}/{Name}.xml` и `{TypePlural}/{Name}/`
|
|
||||||
2. **Проверяет ссылки** (блокирует при наличии, если нет `-Force`):
|
|
||||||
- XML-типы в реквизитах других объектов: `CatalogRef.Имя`, `DocumentRef.Имя` и т.д.
|
|
||||||
- BSL-код: `Справочники.Имя`, `Catalogs.Имя`, вызовы общих модулей
|
|
||||||
- Журналы документов, подписки на события, определяемые типы
|
|
||||||
3. **Удаляет из Configuration.xml**: убирает из `<ChildObjects>`
|
|
||||||
4. **Очищает подсистемы**: рекурсивно удаляет из `<Content>`
|
|
||||||
5. **Удаляет файлы**: XML-файл и каталог объекта
|
|
||||||
|
|
||||||
## Поддерживаемые типы
|
|
||||||
|
|
||||||
Catalog, Document, Enum, Constant, InformationRegister, AccumulationRegister, AccountingRegister, CalculationRegister, ChartOfAccounts, ChartOfCharacteristicTypes, ChartOfCalculationTypes, BusinessProcess, Task, ExchangePlan, DocumentJournal, Report, DataProcessor, CommonModule, ScheduledJob, EventSubscription, HTTPService, WebService, DefinedType, Role, Subsystem, CommonForm, CommonTemplate, CommonPicture, CommonAttribute, SessionParameter, FunctionalOption, FunctionalOptionsParameter, Sequence, FilterCriterion, SettingsStorage, XDTOPackage, WSReference, StyleItem, Language
|
|
||||||
|
|
||||||
## Вывод (объект без ссылок)
|
|
||||||
|
|
||||||
```
|
|
||||||
=== meta-remove: Catalog.Устаревший ===
|
|
||||||
|
|
||||||
[FOUND] Catalogs/Устаревший.xml
|
|
||||||
[FOUND] Catalogs/Устаревший/ (8 files)
|
|
||||||
|
|
||||||
--- Reference check ---
|
|
||||||
[OK] No references found
|
|
||||||
|
|
||||||
--- Configuration.xml ---
|
|
||||||
[OK] Removed <Catalog>Устаревший</Catalog> from ChildObjects
|
|
||||||
[OK] Configuration.xml saved
|
|
||||||
|
|
||||||
--- Subsystems ---
|
|
||||||
[OK] Removed from subsystem 'Справочники'
|
|
||||||
|
|
||||||
--- Files ---
|
|
||||||
[OK] Deleted directory: Catalogs/Устаревший/
|
|
||||||
[OK] Deleted file: Catalogs/Устаревший.xml
|
|
||||||
|
|
||||||
=== Done: 4 actions performed (1 subsystem references removed) ===
|
|
||||||
```
|
|
||||||
|
|
||||||
## Вывод (объект со ссылками — блокировка)
|
|
||||||
|
|
||||||
```
|
|
||||||
=== meta-remove: Catalog.Валюты ===
|
|
||||||
|
|
||||||
[FOUND] Catalogs/Валюты.xml
|
|
||||||
[FOUND] Catalogs/Валюты/ (4 files)
|
|
||||||
|
|
||||||
--- Reference check ---
|
|
||||||
[WARN] Found 3 reference(s) to Catalog.Валюты:
|
|
||||||
|
|
||||||
Documents/СчетНаОплату.xml
|
|
||||||
pattern: CatalogRef.Валюты
|
|
||||||
InformationRegisters/КурсыВалют.xml
|
|
||||||
pattern: CatalogRef.Валюты
|
|
||||||
CommonModules/РаботаСВалютами/Ext/Module.bsl
|
|
||||||
pattern: Справочники.Валюты
|
|
||||||
|
|
||||||
[ERROR] Cannot remove: object has 3 reference(s).
|
|
||||||
Use -Force to remove anyway, or fix references first.
|
|
||||||
```
|
|
||||||
|
|
||||||
Код возврата: 0 = успешно, 1 = ошибки или найдены ссылки.
|
|
||||||
|
|
||||||
## Проверяемые ссылки
|
|
||||||
|
|
||||||
| Категория | Паттерны поиска |
|
|
||||||
|-----------|----------------|
|
|
||||||
| XML-типы реквизитов | `CatalogRef.Name`, `DocumentRef.Name`, `EnumRef.Name` и др. |
|
|
||||||
| BSL-код (рус.) | `Справочники.Name`, `Документы.Name`, `Перечисления.Name` и др. |
|
|
||||||
| BSL-код (англ.) | `Catalogs.Name`, `Documents.Name`, `Enums.Name` и др. |
|
|
||||||
| Общие модули | `Name.` (вызовы методов), `<Handler>Name.`, `<MethodName>Name.` |
|
|
||||||
|
|
||||||
Ссылки из Configuration.xml, ConfigDumpInfo.xml и подсистем НЕ считаются блокирующими — они очищаются автоматически.
|
|
||||||
|
|
||||||
## Примеры
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# Проверка ссылок + dry run
|
|
||||||
... -ConfigDir C:\WS\tasks\cfsrc\acc_8.3.24 -Object "Catalog.Устаревший" -DryRun
|
|
||||||
|
|
||||||
# Удалить объект без ссылок
|
|
||||||
... -ConfigDir C:\WS\tasks\cfsrc\acc_8.3.24 -Object "Catalog.Устаревший"
|
|
||||||
|
|
||||||
# Принудительно удалить несмотря на ссылки
|
|
||||||
... -ConfigDir C:\WS\tasks\cfsrc\acc_8.3.24 -Object "Catalog.Устаревший" -Force
|
|
||||||
|
|
||||||
# Только дерегистрировать (файлы оставить)
|
|
||||||
... -ConfigDir C:\WS\tasks\cfsrc\acc_8.3.24 -Object "Report.Старый" -KeepFiles
|
|
||||||
|
|
||||||
# Удалить общий модуль
|
|
||||||
... -ConfigDir src -Object "CommonModule.МойМодуль"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Когда использовать
|
|
||||||
|
|
||||||
- **Рефакторинг**: удаление неиспользуемых объектов
|
|
||||||
- **Очистка**: удаление временных/тестовых объектов
|
|
||||||
- **Перенос**: удаление объекта перед пересозданием с другой структурой
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
---
|
|
||||||
name: meta-validate
|
|
||||||
description: Валидация объекта метаданных 1С. Используй после создания или модификации объекта конфигурации для проверки корректности
|
|
||||||
argument-hint: <ObjectPath> [-Detailed] [-MaxErrors 30] — pipe-separated paths for batch
|
|
||||||
allowed-tools:
|
|
||||||
- Bash
|
|
||||||
- Read
|
|
||||||
- Glob
|
|
||||||
---
|
|
||||||
|
|
||||||
# /meta-validate — валидация объекта метаданных 1С
|
|
||||||
|
|
||||||
Проверяет XML объекта метаданных из выгрузки конфигурации на структурные ошибки.
|
|
||||||
|
|
||||||
## Параметры
|
|
||||||
|
|
||||||
| Параметр | Обяз. | Умолч. | Описание |
|
|
||||||
|------------|:-----:|---------|-------------------------------------------------|
|
|
||||||
| ObjectPath | да | — | Путь к XML-файлу или каталогу. Через `\|` для batch |
|
|
||||||
| Detailed | нет | — | Показывать [OK] для каждой проверки |
|
|
||||||
| MaxErrors | нет | 30 | Остановиться после N ошибок (per object) |
|
|
||||||
| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) |
|
|
||||||
|
|
||||||
## Команда
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
powershell.exe -NoProfile -File .claude/skills/meta-validate/scripts/meta-validate.ps1 -ObjectPath "Catalogs/Номенклатура/Номенклатура.xml"
|
|
||||||
powershell.exe -NoProfile -File .claude/skills/meta-validate/scripts/meta-validate.ps1 -ObjectPath "Catalogs/Банки|Documents/Заказ"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Поддерживаемые типы (23)
|
|
||||||
|
|
||||||
**Ссылочные:** Catalog, Document, Enum, ExchangePlan, ChartOfAccounts, ChartOfCharacteristicTypes, ChartOfCalculationTypes, BusinessProcess, Task
|
|
||||||
**Регистры:** InformationRegister, AccumulationRegister, AccountingRegister, CalculationRegister
|
|
||||||
**Отчёты/Обработки:** Report, DataProcessor
|
|
||||||
**Сервисные:** CommonModule, ScheduledJob, EventSubscription, HTTPService, WebService
|
|
||||||
**Прочие:** Constant, DocumentJournal, DefinedType
|
|
||||||
|
|
||||||
## Проверки
|
|
||||||
|
|
||||||
| # | Проверка | Серьёзность |
|
|
||||||
|----|------------------------------------------|--------------|
|
|
||||||
| 1 | XML well-formedness + root structure | ERROR |
|
|
||||||
| 2 | InternalInfo / GeneratedType | ERROR / WARN |
|
|
||||||
| 3 | Properties — Name, Synonym | ERROR / WARN |
|
|
||||||
| 4 | Properties — enum-значения свойств | ERROR |
|
|
||||||
| 5 | StandardAttributes | ERROR / WARN |
|
|
||||||
| 6 | ChildObjects — допустимые элементы | ERROR |
|
|
||||||
| 7 | Attributes/Dimensions/Resources — UUID, Name, Type | ERROR |
|
|
||||||
| 7b | Reserved attribute names | WARN |
|
|
||||||
| 8 | Уникальность имён | ERROR |
|
|
||||||
| 9 | TabularSections — внутренняя структура | ERROR / WARN |
|
|
||||||
| 10 | Кросс-свойства | ERROR / WARN |
|
|
||||||
| 11 | HTTPService/WebService — вложенная структура | ERROR |
|
|
||||||
| 12 | Forbidden properties per type | ERROR |
|
|
||||||
| 13 | Method reference (Handler/MethodName) | ERROR / WARN |
|
|
||||||
| 14 | DocumentJournal Columns | ERROR |
|
|
||||||
|
|
||||||
Exit code: 0 = OK, 1 = есть ошибки. По умолчанию краткий вывод. `-Detailed` для поштучной детализации.
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
---
|
|
||||||
name: mxl-validate
|
|
||||||
description: Валидация макета табличного документа (MXL). Используй после создания или модификации макета для проверки корректности
|
|
||||||
argument-hint: <TemplatePath> [-Detailed] [-MaxErrors 20]
|
|
||||||
allowed-tools:
|
|
||||||
- Bash
|
|
||||||
- Read
|
|
||||||
- Glob
|
|
||||||
---
|
|
||||||
|
|
||||||
# /mxl-validate — валидация макета табличного документа (MXL)
|
|
||||||
|
|
||||||
Проверяет Template.xml на структурные ошибки: индексы, ссылки на палитры, диапазоны именованных областей и объединений.
|
|
||||||
|
|
||||||
## Параметры
|
|
||||||
|
|
||||||
| Параметр | Обяз. | Умолч. | Описание |
|
|
||||||
|---------------|:-----:|---------|--------------------------------------------|
|
|
||||||
| TemplatePath | да | — | Путь к макету (директория или Template.xml) |
|
|
||||||
| Detailed | нет | — | Показывать [OK] для каждой проверки |
|
|
||||||
| MaxErrors | нет | 20 | Остановиться после N ошибок |
|
|
||||||
|
|
||||||
## Команда
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
powershell.exe -NoProfile -File .claude/skills/mxl-validate/scripts/mxl-validate.ps1 -TemplatePath "Catalogs/Номенклатура/Templates/Макет"
|
|
||||||
powershell.exe -NoProfile -File .claude/skills/mxl-validate/scripts/mxl-validate.ps1 -TemplatePath "src/МояОбработка/Templates/ПечатнаяФорма"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Проверки
|
|
||||||
|
|
||||||
| # | Проверка | Серьёзность |
|
|
||||||
|---|---|---|
|
|
||||||
| 1 | `<height>` >= максимальный индекс строки + 1 | ERROR |
|
|
||||||
| 2 | `<vgRows>` <= `<height>` | WARN |
|
|
||||||
| 3 | Индексы форматов ячеек (`<f>`) в пределах палитры форматов | ERROR |
|
|
||||||
| 4 | `<formatIndex>` строк и колонок в пределах палитры | ERROR |
|
|
||||||
| 5 | Индексы колонок в ячейках (`<i>`) в пределах количества колонок (с учётом набора) | ERROR |
|
|
||||||
| 6 | `<columnsID>` строк ссылается на существующий набор колонок | ERROR |
|
|
||||||
| 7 | `<columnsID>` в merge/namedItem ссылается на существующий набор | ERROR |
|
|
||||||
| 8 | Диапазоны именованных областей в пределах границ документа | ERROR |
|
|
||||||
| 9 | Диапазоны объединений в пределах границ документа | ERROR |
|
|
||||||
| 10 | Индексы шрифтов в форматах в пределах палитры шрифтов | ERROR |
|
|
||||||
| 11 | Индексы линий границ в форматах в пределах палитры линий | ERROR |
|
|
||||||
| 12 | `pictureIndex` рисунков ссылается на существующую картинку | ERROR |
|
|
||||||
|
|
||||||
Exit code: 0 = OK, 1 = есть ошибки. По умолчанию краткий вывод. `-Detailed` для поштучной детализации.
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
---
|
|
||||||
name: role-validate
|
|
||||||
description: Валидация роли 1С. Используй после создания или модификации роли для проверки корректности
|
|
||||||
argument-hint: <RightsPath> [-Detailed] [-MaxErrors 30]
|
|
||||||
allowed-tools:
|
|
||||||
- Bash
|
|
||||||
- Read
|
|
||||||
---
|
|
||||||
|
|
||||||
# /role-validate — валидация роли 1С
|
|
||||||
|
|
||||||
Проверяет корректность `Rights.xml` роли: формат XML, namespace, глобальные флаги, типы объектов, имена прав, RLS-ограничения, шаблоны. Опционально проверяет метаданные роли (UUID, имя, синоним).
|
|
||||||
|
|
||||||
## Параметры
|
|
||||||
|
|
||||||
| Параметр | Обяз. | Умолч. | Описание |
|
|
||||||
|--------------|:-----:|---------|-------------------------------------------------|
|
|
||||||
| RightsPath | да | — | Путь к роли (директория или `Rights.xml`) |
|
|
||||||
| Detailed | нет | — | Показывать [OK] для каждой проверки |
|
|
||||||
| MaxErrors | нет | 30 | Макс. ошибок до остановки (по умолчанию 30) |
|
|
||||||
| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) |
|
|
||||||
|
|
||||||
## Команда
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
powershell.exe -NoProfile -File .claude/skills/role-validate/scripts/role-validate.ps1 -RightsPath "Roles/МояРоль"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Проверки
|
|
||||||
|
|
||||||
| # | Проверка | Серьёзность |
|
|
||||||
|---|----------|-------------|
|
|
||||||
| 1 | XML well-formed — парсинг без ошибок | ERROR |
|
|
||||||
| 2 | Корневой элемент `<Rights>` с namespace `http://v8.1c.ru/8.2/roles` | ERROR |
|
|
||||||
| 3 | Три глобальных флага: setForNewObjects, setForAttributesByDefault, independentRightsOfChildObjects | ERROR |
|
|
||||||
| 4 | Объекты: name не пуст, тип распознан, права валидны для типа (с подсказкой при опечатке) | ERROR/WARN |
|
|
||||||
| 5 | Вложенные объекты (3+ сегмента): допустимы только View, Edit (или Use для IntegrationServiceChannel) | ERROR |
|
|
||||||
| 6 | RLS `<restrictionByCondition>`: condition не пуст | ERROR |
|
|
||||||
| 7 | Шаблоны `<restrictionTemplate>`: name и condition не пусты | ERROR |
|
|
||||||
| 8 | Метаданные (если MetadataPath): UUID, Name, Synonym | ERROR/WARN |
|
|
||||||
|
|
||||||
Exit code: 0 = OK, 1 = есть ошибки. По умолчанию краткий вывод. `-Detailed` для поштучной детализации.
|
|
||||||
@@ -1,299 +0,0 @@
|
|||||||
---
|
|
||||||
name: skd-compile
|
|
||||||
description: Компиляция схемы компоновки данных 1С (СКД) из компактного JSON-определения. Используй когда нужно создать СКД с нуля
|
|
||||||
argument-hint: "[-DefinitionFile <json> | -Value <json-string>] -OutputPath <Template.xml>"
|
|
||||||
allowed-tools:
|
|
||||||
- Bash
|
|
||||||
- Read
|
|
||||||
- Write
|
|
||||||
- Glob
|
|
||||||
---
|
|
||||||
|
|
||||||
# /skd-compile — генерация СКД из JSON DSL
|
|
||||||
|
|
||||||
Принимает JSON-определение схемы компоновки данных → генерирует Template.xml (DataCompositionSchema).
|
|
||||||
|
|
||||||
## Параметры и команда
|
|
||||||
|
|
||||||
| Параметр | Описание |
|
|
||||||
|----------|----------|
|
|
||||||
| `DefinitionFile` | Путь к JSON-файлу с определением СКД (взаимоисключающий с Value) |
|
|
||||||
| `Value` | JSON-строка с определением СКД (взаимоисключающий с DefinitionFile) |
|
|
||||||
| `OutputPath` | Путь к выходному Template.xml |
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# Из файла
|
|
||||||
powershell.exe -NoProfile -File .claude/skills/skd-compile/scripts/skd-compile.ps1 -DefinitionFile "<json>" -OutputPath "<Template.xml>"
|
|
||||||
|
|
||||||
# Из строки (без промежуточного файла)
|
|
||||||
powershell.exe -NoProfile -File .claude/skills/skd-compile/scripts/skd-compile.ps1 -Value '<json-string>' -OutputPath "<Template.xml>"
|
|
||||||
```
|
|
||||||
|
|
||||||
## JSON DSL — краткий справочник
|
|
||||||
|
|
||||||
Справочник ниже. Все примеры компилируемы как есть.
|
|
||||||
|
|
||||||
### Корневая структура
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"dataSets": [...],
|
|
||||||
"calculatedFields": [...],
|
|
||||||
"totalFields": [...],
|
|
||||||
"parameters": [...],
|
|
||||||
"templates": [...],
|
|
||||||
"groupTemplates": [...],
|
|
||||||
"dataSetLinks": [...],
|
|
||||||
"settingsVariants": [...]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Умолчания: `dataSources` → авто `ИсточникДанных1/Local`; `settingsVariants` → авто "Основной" с деталями.
|
|
||||||
|
|
||||||
### Наборы данных
|
|
||||||
|
|
||||||
Тип по ключу: `query` → DataSetQuery, `objectName` → DataSetObject, `items` → DataSetUnion.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "name": "Продажи", "query": "ВЫБРАТЬ ...", "fields": [...] }
|
|
||||||
```
|
|
||||||
|
|
||||||
Запрос поддерживает `@file` — ссылку на внешний .sql файл вместо inline-текста: `"query": "@queries/sales.sql"`. Путь разрешается относительно JSON-файла, затем CWD.
|
|
||||||
|
|
||||||
### Поля — shorthand и объектная форма
|
|
||||||
|
|
||||||
```
|
|
||||||
"Наименование" — просто имя
|
|
||||||
"Количество: decimal(15,2)" — имя + тип
|
|
||||||
"Организация: CatalogRef.Организации @dimension" — + роль
|
|
||||||
"Служебное: string #noFilter #noOrder" — + ограничения
|
|
||||||
```
|
|
||||||
|
|
||||||
Объектная форма — когда нужен title или другие свойства:
|
|
||||||
```json
|
|
||||||
{ "field": "ОстатокНаНачалоПериода", "title": "Остаток на начало периода" }
|
|
||||||
```
|
|
||||||
`dataPath` автоматически берётся из `field`, если не указан явно.
|
|
||||||
|
|
||||||
Типы: `string`, `string(N)`, `decimal(D,F)`, `boolean`, `date`, `dateTime`, `CatalogRef.X`, `DocumentRef.X`, `EnumRef.X`, `StandardPeriod`. Ссылочные типы эмитируются с inline namespace `d5p1:` (`http://v8.1c.ru/8.1/data/enterprise/current-config`). Сборка EPF со ссылочными типами требует базу с соответствующей конфигурацией.
|
|
||||||
|
|
||||||
**Синонимы типов** (русские и альтернативные): `число` = decimal, `строка` = string, `булево` = boolean, `дата` = date, `датаВремя` = dateTime, `СтандартныйПериод` = StandardPeriod, `СправочникСсылка.X` = CatalogRef.X, `ДокументСсылка.X` = DocumentRef.X, `int`/`number` = decimal, `bool` = boolean. Регистронезависимые.
|
|
||||||
|
|
||||||
Роли: `@dimension`, `@account`, `@balance`, `@period`.
|
|
||||||
|
|
||||||
Ограничения: `#noField`, `#noFilter`, `#noGroup`, `#noOrder`.
|
|
||||||
|
|
||||||
### Итоги (shorthand)
|
|
||||||
|
|
||||||
```json
|
|
||||||
"totalFields": ["Количество: Сумма", "Стоимость: Сумма(Кол * Цена)"]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Параметры (shorthand + @autoDates)
|
|
||||||
|
|
||||||
```json
|
|
||||||
"parameters": [
|
|
||||||
"Период: StandardPeriod = LastMonth @autoDates"
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
`@autoDates` — автоматически генерирует параметры `ДатаНачала` и `ДатаОкончания` с выражениями `&Период.ДатаНачала` / `&Период.ДатаОкончания` и `availableAsField=false`. Заменяет 5 строк на 1.
|
|
||||||
|
|
||||||
### Фильтры — shorthand
|
|
||||||
|
|
||||||
```json
|
|
||||||
"filter": [
|
|
||||||
"Организация = _ @off @user",
|
|
||||||
"Дата >= 2024-01-01T00:00:00",
|
|
||||||
"Статус filled"
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
Формат: `"Поле оператор значение @флаги"`. Значение `_` = пустое (placeholder). Флаги: `@off` (use=false), `@user` (userSettingID=auto), `@quickAccess`, `@normal`, `@inaccessible`.
|
|
||||||
|
|
||||||
В объектной форме доступны: `viewMode`, `userSettingID`, `userSettingPresentation`.
|
|
||||||
|
|
||||||
Группы фильтров (Or/And/Not):
|
|
||||||
```json
|
|
||||||
{ "group": "Or", "items": [
|
|
||||||
{ "group": "And", "items": [
|
|
||||||
{ "field": "Статус", "op": "=", "value": "Активен" },
|
|
||||||
{ "field": "Сумма", "op": ">", "value": 1000 }
|
|
||||||
]},
|
|
||||||
{ "field": "Количество", "op": "filled" }
|
|
||||||
]}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Параметры данных — shorthand
|
|
||||||
|
|
||||||
```json
|
|
||||||
"dataParameters": [
|
|
||||||
"Период = LastMonth @user",
|
|
||||||
"Организация @off @user"
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
Формат: `"Имя [= значение] @флаги"`. Для StandardPeriod варианты (LastMonth, ThisYear и т.д.) распознаются автоматически.
|
|
||||||
|
|
||||||
### Структура — string shorthand
|
|
||||||
|
|
||||||
```json
|
|
||||||
"structure": "Организация > details"
|
|
||||||
"structure": "Организация > Номенклатура > details"
|
|
||||||
```
|
|
||||||
|
|
||||||
`>` разделяет уровни группировки. `details` (или `детали`) = детальные записи. `selection` и `order` по умолчанию `["Auto"]` на каждом уровне.
|
|
||||||
|
|
||||||
Для сложных случаев (таблицы, диаграммы, фильтры на уровне группировки) используется объектная форма.
|
|
||||||
|
|
||||||
### Варианты настроек
|
|
||||||
|
|
||||||
```json
|
|
||||||
"settingsVariants": [{
|
|
||||||
"name": "Основной",
|
|
||||||
"title": "Продажи по организациям",
|
|
||||||
"settings": {
|
|
||||||
"selection": ["Номенклатура", "Количество", "Auto"],
|
|
||||||
"filter": ["Организация = _ @off @user"],
|
|
||||||
"order": ["Количество desc", "Auto"],
|
|
||||||
"conditionalAppearance": [
|
|
||||||
{
|
|
||||||
"filter": ["Просрочено = true"],
|
|
||||||
"appearance": { "ЦветТекста": "style:ПросроченныеДанныеЦвет" },
|
|
||||||
"presentation": "Выделять просроченные",
|
|
||||||
"viewMode": "Normal",
|
|
||||||
"userSettingID": "auto"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"outputParameters": { "Заголовок": "Мой отчёт" },
|
|
||||||
"dataParameters": ["Период = LastMonth @user"],
|
|
||||||
"structure": "Организация > details"
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Условное оформление (conditionalAppearance)
|
|
||||||
|
|
||||||
```json
|
|
||||||
"conditionalAppearance": [
|
|
||||||
{
|
|
||||||
"selection": ["Поле1"],
|
|
||||||
"filter": ["Поле1 notFilled"],
|
|
||||||
"appearance": { "Текст": "Не указано", "ЦветТекста": "style:XXX" },
|
|
||||||
"presentation": "Описание",
|
|
||||||
"viewMode": "Normal",
|
|
||||||
"userSettingID": "auto"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
Типы значений appearance: `style:XXX`/`web:XXX`/`win:XXX` → Color, `true`/`false` → Boolean, параметр `Текст` → LocalStringType, прочее → String.
|
|
||||||
|
|
||||||
### Итоги с привязкой к группировкам
|
|
||||||
|
|
||||||
```json
|
|
||||||
"totalFields": [
|
|
||||||
{ "dataPath": "Кол", "expression": "Сумма(Кол)", "group": ["Группа1", "Группа1 Иерархия", "ОбщийИтог"] }
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Шаблоны вывода — компактный DSL
|
|
||||||
|
|
||||||
Вместо raw XML (`template`) — табличное описание через `rows` + именованный стиль `style`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
"templates": [
|
|
||||||
{
|
|
||||||
"name": "Макет1",
|
|
||||||
"style": "header",
|
|
||||||
"widths": [36, 33, 16, 17],
|
|
||||||
"minHeight": 24.75,
|
|
||||||
"rows": [
|
|
||||||
["Виды кассы", "Валюта", "Остаток на начало\nпериода", "Остаток на\nконец периода"],
|
|
||||||
["|", "|", "|", "|"],
|
|
||||||
["К1", "К2", "К3", "К4"]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Макет2",
|
|
||||||
"style": "data",
|
|
||||||
"widths": [36, 33, 16, 17],
|
|
||||||
"rows": [["{ВидКассы}", "{Валюта}", "{Остаток}", "{ОстатокКонец}"]],
|
|
||||||
"parameters": [
|
|
||||||
{ "name": "ВидКассы", "expression": "Представление(Счет)" },
|
|
||||||
{ "name": "Остаток", "expression": "ОстатокНаНачалоПериода" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
Синтаксис ячеек: `"текст"` — статика, `"{Имя}"` — параметр, `"|"` — объединение с ячейкой выше, `null` — пустая.
|
|
||||||
|
|
||||||
Встроенные стили: `header` (фон, центр, перенос), `data` (фон группы), `subheader` (без фона, центр), `total` (без фона). Все — Arial 10, рамки Solid 1px, цвета через стили платформы.
|
|
||||||
|
|
||||||
Пользовательские стили: файл `skd-styles.json` рядом с JSON или в корне проекта. Все допустимые ключи и формат цветов — в `examples/skd-styles.json`.
|
|
||||||
|
|
||||||
Raw XML (`"template": "<...>"`) остаётся как fallback. Детект: если есть `rows` — DSL, иначе — raw.
|
|
||||||
|
|
||||||
### Привязки макетов к группировкам
|
|
||||||
|
|
||||||
```json
|
|
||||||
"groupTemplates": [
|
|
||||||
{ "groupField": "Счет", "templateType": "GroupHeader", "template": "Макет1" },
|
|
||||||
{ "groupField": "Счет", "templateType": "Header", "template": "Макет2" }
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Примеры
|
|
||||||
|
|
||||||
### Минимальный
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"dataSets": [{
|
|
||||||
"query": "ВЫБРАТЬ Номенклатура.Наименование КАК Наименование ИЗ Справочник.Номенклатура КАК Номенклатура",
|
|
||||||
"fields": ["Наименование"]
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### С запросом из внешнего файла (@file)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"dataSets": [{
|
|
||||||
"query": "@queries/sales.sql",
|
|
||||||
"fields": ["Номенклатура: СправочникСсылка.Номенклатура @dimension", "Количество: число(15,3)", "Сумма: число(15,2)"]
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### С ресурсами, параметрами и @autoDates
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"dataSets": [{
|
|
||||||
"query": "ВЫБРАТЬ Продажи.Номенклатура, Продажи.Количество, Продажи.Сумма ИЗ РегистрНакопления.Продажи КАК Продажи",
|
|
||||||
"fields": ["Номенклатура: СправочникСсылка.Номенклатура @dimension", "Количество: число(15,3)", "Сумма: число(15,2)"]
|
|
||||||
}],
|
|
||||||
"totalFields": ["Количество: Сумма", "Сумма: Сумма"],
|
|
||||||
"parameters": ["Период: СтандартныйПериод = LastMonth @autoDates"],
|
|
||||||
"settingsVariants": [{
|
|
||||||
"name": "Основной",
|
|
||||||
"settings": {
|
|
||||||
"selection": ["Номенклатура", "Количество", "Сумма", "Auto"],
|
|
||||||
"filter": ["Организация = _ @off @user"],
|
|
||||||
"dataParameters": ["Период = LastMonth @user"],
|
|
||||||
"structure": "Организация > details"
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Верификация
|
|
||||||
|
|
||||||
```
|
|
||||||
/skd-validate <OutputPath> — валидация структуры XML
|
|
||||||
/skd-info <OutputPath> — визуальная сводка
|
|
||||||
/skd-info <OutputPath> -Mode variant -Name 1 — проверка варианта настроек
|
|
||||||
```
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,218 +0,0 @@
|
|||||||
---
|
|
||||||
name: skd-edit
|
|
||||||
description: Точечное редактирование схемы компоновки данных 1С (СКД). Используй когда нужно модифицировать существующую СКД — добавить поля, итоги, фильтры, параметры, изменить текст запроса
|
|
||||||
argument-hint: <TemplatePath> -Operation <op> -Value <value>
|
|
||||||
allowed-tools:
|
|
||||||
- Bash
|
|
||||||
- Read
|
|
||||||
- Write
|
|
||||||
- Glob
|
|
||||||
---
|
|
||||||
|
|
||||||
# /skd-edit — точечное редактирование СКД (Template.xml)
|
|
||||||
|
|
||||||
Атомарные операции модификации существующей схемы компоновки данных: добавление, удаление и модификация полей, итогов, фильтров, параметров, настроек варианта, управление структурой, замена запроса.
|
|
||||||
|
|
||||||
## Параметры и команда
|
|
||||||
|
|
||||||
| Параметр | Описание |
|
|
||||||
|----------|----------|
|
|
||||||
| `TemplatePath` | Путь к Template.xml (или к папке — автодополнение Ext/Template.xml) |
|
|
||||||
| `Operation` | Операция (см. список ниже) |
|
|
||||||
| `Value` | Значение операции (shorthand-строка или текст запроса) |
|
|
||||||
| `DataSet` | (опц.) Имя набора данных (умолч. первый) |
|
|
||||||
| `Variant` | (опц.) Имя варианта настроек (умолч. первый) |
|
|
||||||
| `NoSelection` | (опц.) Не добавлять поле в selection варианта |
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
powershell.exe -NoProfile -File .claude/skills/skd-edit/scripts/skd-edit.ps1 -TemplatePath "<path>" -Operation <op> -Value "<value>"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Пакетный режим (batch)
|
|
||||||
|
|
||||||
Несколько значений в одном вызове через разделитель `;;`:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
-Operation add-field -Value "Цена: decimal(15,2) ;; Количество: decimal(15,3) ;; Сумма: decimal(15,2)"
|
|
||||||
```
|
|
||||||
|
|
||||||
Работает для всех операций кроме `set-query`, `set-structure` и `add-dataSet`.
|
|
||||||
|
|
||||||
## Операции
|
|
||||||
|
|
||||||
### add-field — добавить поле в набор данных
|
|
||||||
|
|
||||||
Shorthand: `"Имя [Заголовок]: тип @роль #ограничение"`.
|
|
||||||
|
|
||||||
```
|
|
||||||
"Цена: decimal(15,2)"
|
|
||||||
"Организация [Орг-ция]: CatalogRef.Организации @dimension"
|
|
||||||
"Служебное: string #noFilter #noOrder"
|
|
||||||
```
|
|
||||||
|
|
||||||
Поле добавляется в набор и в selection варианта (если нет `-NoSelection`). Дубликат dataPath — предупреждение, пропуск.
|
|
||||||
|
|
||||||
### add-total — добавить итог
|
|
||||||
|
|
||||||
```
|
|
||||||
"Цена: Среднее"
|
|
||||||
"Стоимость: Сумма(Кол * Цена)"
|
|
||||||
```
|
|
||||||
|
|
||||||
### add-calculated-field — добавить вычисляемое поле
|
|
||||||
|
|
||||||
Shorthand: `"Имя [Заголовок]: тип = Выражение"`.
|
|
||||||
|
|
||||||
```
|
|
||||||
"Маржа = Продажа - Закупка"
|
|
||||||
"Наценка [Наценка, %]: decimal(10,2) = Маржа / Закупка * 100"
|
|
||||||
```
|
|
||||||
|
|
||||||
Также добавляется в selection варианта.
|
|
||||||
|
|
||||||
### add-parameter — добавить параметр
|
|
||||||
|
|
||||||
```
|
|
||||||
"Период: StandardPeriod = LastMonth @autoDates"
|
|
||||||
"Организация: CatalogRef.Организации"
|
|
||||||
```
|
|
||||||
|
|
||||||
`@autoDates` генерирует `ДатаНачала` и `ДатаОкончания` автоматически.
|
|
||||||
|
|
||||||
### add-filter — добавить фильтр в вариант
|
|
||||||
|
|
||||||
Shorthand: `"Поле оператор значение @флаги"`. Флаги: `@off`, `@user`, `@quickAccess`, `@normal`, `@inaccessible`.
|
|
||||||
|
|
||||||
```
|
|
||||||
"Номенклатура = _ @off @user"
|
|
||||||
"Дата >= 2024-01-01T00:00:00"
|
|
||||||
"Статус filled"
|
|
||||||
```
|
|
||||||
|
|
||||||
### add-dataParameter — добавить параметр данных в вариант
|
|
||||||
|
|
||||||
Shorthand: `"Имя [= значение] @флаги"`.
|
|
||||||
|
|
||||||
```
|
|
||||||
"Период = LastMonth @user"
|
|
||||||
"Организация @off @user"
|
|
||||||
```
|
|
||||||
|
|
||||||
### add-order — добавить сортировку
|
|
||||||
|
|
||||||
Shorthand: `"Поле [desc]"`. По умолчанию asc. `Auto` — авто-элемент.
|
|
||||||
|
|
||||||
```
|
|
||||||
"Количество desc"
|
|
||||||
"Auto"
|
|
||||||
```
|
|
||||||
|
|
||||||
### add-selection — добавить элемент выборки
|
|
||||||
|
|
||||||
```
|
|
||||||
"Номенклатура"
|
|
||||||
"Auto"
|
|
||||||
```
|
|
||||||
|
|
||||||
### add-dataSetLink — добавить связь наборов данных
|
|
||||||
|
|
||||||
Shorthand: `"Источник > Приёмник on ВырИсточника = ВырПриёмника [param Имя]"`.
|
|
||||||
|
|
||||||
```
|
|
||||||
"Набор1 > Набор2 on Поле1 = Поле2"
|
|
||||||
"Набор1 > Набор2 on Поле1 = Поле2 [param Связь]"
|
|
||||||
```
|
|
||||||
|
|
||||||
### add-dataSet — добавить набор данных
|
|
||||||
|
|
||||||
Shorthand: `"Имя: ТЕКСТ_ЗАПРОСА"` или `"ТЕКСТ_ЗАПРОСА"` (авто-имя `НаборДанныхN`).
|
|
||||||
|
|
||||||
```
|
|
||||||
"Доп: ВЫБРАТЬ 1 КАК Тест"
|
|
||||||
"ВЫБРАТЬ Ссылка ИЗ Справочник.Номенклатура"
|
|
||||||
"Продажи: @queries/sales.sql"
|
|
||||||
```
|
|
||||||
|
|
||||||
`dataSource` берётся из первого существующего. Дубликат имени — предупреждение, пропуск. Не поддерживает пакетный режим (запрос может содержать `;;`).
|
|
||||||
|
|
||||||
### add-variant — добавить вариант настроек
|
|
||||||
|
|
||||||
Shorthand: `"Имя [Представление]"`. Представление опционально, по умолчанию = имя.
|
|
||||||
|
|
||||||
```
|
|
||||||
"Детальный"
|
|
||||||
"Детальный [Детальный отчёт]"
|
|
||||||
```
|
|
||||||
|
|
||||||
Создаёт вариант с Auto selection + detail group. Дубликат имени — предупреждение, пропуск.
|
|
||||||
|
|
||||||
### add-conditionalAppearance — добавить условное оформление
|
|
||||||
|
|
||||||
Shorthand: `"Параметр = значение [when условие] [for Поле1, Поле2]"`. Блок `when` — синтаксис `add-filter` (Поле оператор значение).
|
|
||||||
|
|
||||||
```
|
|
||||||
"ЦветТекста = web:Red when Сумма < 0"
|
|
||||||
"ЦветФона = web:LightGreen when Статус = Одобрен for Статус"
|
|
||||||
"МинимальнаяШирина = 50 for Организация"
|
|
||||||
"Формат = ЧДЦ=2 for Цена, Сумма"
|
|
||||||
```
|
|
||||||
|
|
||||||
Типы значений (автодетект): `web:*`/`style:*`/`win:*` → цвет, `true`/`false` → boolean, иначе строка.
|
|
||||||
|
|
||||||
### set-query — заменить текст запроса
|
|
||||||
|
|
||||||
Не поддерживает пакетный режим. Value — полный текст запроса или `@path/to/file.sql` (ссылка на внешний файл). Путь разрешается относительно Template.xml, затем CWD.
|
|
||||||
|
|
||||||
### set-outputParameter — установить параметр вывода
|
|
||||||
|
|
||||||
```
|
|
||||||
"Заголовок = Мой отчёт"
|
|
||||||
"ВыводитьЗаголовок = true"
|
|
||||||
```
|
|
||||||
|
|
||||||
Если параметр уже существует — заменяет значение.
|
|
||||||
|
|
||||||
### set-structure — установить структуру варианта
|
|
||||||
|
|
||||||
Shorthand: `"Поле1 > Поле2 > details"`. `details`/`детали` — детальные записи. Заменяет всю структуру. Не поддерживает пакетный режим.
|
|
||||||
|
|
||||||
```
|
|
||||||
"Организация > Номенклатура > details"
|
|
||||||
"details"
|
|
||||||
```
|
|
||||||
|
|
||||||
### modify-field — изменить существующее поле
|
|
||||||
|
|
||||||
Тот же shorthand что и `add-field`. Находит по dataPath, объединяет свойства (непустые переопределяют), сохраняет позицию.
|
|
||||||
|
|
||||||
```
|
|
||||||
"Цена [Цена USD]: decimal(10,4) @dimension"
|
|
||||||
```
|
|
||||||
|
|
||||||
### modify-filter — изменить существующий фильтр
|
|
||||||
|
|
||||||
Тот же shorthand что и `add-filter`. Находит по полю, обновляет оператор/значение/флаги.
|
|
||||||
|
|
||||||
### modify-dataParameter — изменить параметр данных
|
|
||||||
|
|
||||||
Тот же shorthand что и `add-dataParameter`. Находит по имени, обновляет значение/флаги.
|
|
||||||
|
|
||||||
### remove-* и clear-*
|
|
||||||
|
|
||||||
| Операция | Value | Действие |
|
|
||||||
|----------|-------|----------|
|
|
||||||
| `remove-field` | dataPath | Удаляет поле из набора + из selection варианта |
|
|
||||||
| `remove-total` | dataPath | Удаляет итог |
|
|
||||||
| `remove-calculated-field` | dataPath | Удаляет вычисляемое поле + из selection |
|
|
||||||
| `remove-parameter` | name | Удаляет параметр |
|
|
||||||
| `remove-filter` | поле | Удаляет первый фильтр с указанным полем |
|
|
||||||
| `clear-selection` | `*` | Очищает все элементы selection |
|
|
||||||
| `clear-order` | `*` | Очищает все элементы order |
|
|
||||||
| `clear-filter` | `*` | Очищает все элементы filter |
|
|
||||||
|
|
||||||
## Верификация
|
|
||||||
|
|
||||||
```
|
|
||||||
/skd-validate <TemplatePath> — валидация структуры после редактирования
|
|
||||||
/skd-info <TemplatePath> — визуальная сводка
|
|
||||||
```
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,246 +0,0 @@
|
|||||||
# /skd-info — полная справка по режимам
|
|
||||||
|
|
||||||
Компактное описание — в [SKILL.md](SKILL.md).
|
|
||||||
|
|
||||||
## overview (по умолчанию) — карта схемы
|
|
||||||
|
|
||||||
Компактная навигационная карта (10-25 строк). Показывает структуру и подсказывает следующие шаги:
|
|
||||||
|
|
||||||
```
|
|
||||||
=== DCS: ОсновнаяСхемаКомпоновкиДанных (362 lines) ===
|
|
||||||
|
|
||||||
Sources: ИсточникДанных1 (Local)
|
|
||||||
|
|
||||||
Datasets:
|
|
||||||
[Query] НоменклатураСЦенами 7 fields, query 40 lines
|
|
||||||
Calculated: 1
|
|
||||||
Resources: 1
|
|
||||||
Templates: 1 templates, 1 group bindings
|
|
||||||
Params: (none)
|
|
||||||
|
|
||||||
Variants:
|
|
||||||
[1] НоменклатураИЦены "Номенклатура и цены" Table(detail) 3 filters
|
|
||||||
[2] НоменклатураБезЦен "Номенклатура без цен" Group(detail) 2 filters
|
|
||||||
|
|
||||||
Next:
|
|
||||||
-Mode query query text
|
|
||||||
-Mode fields field tables by dataset
|
|
||||||
-Mode calculated calculated field expressions
|
|
||||||
-Mode resources resource aggregation
|
|
||||||
-Mode variant -Name <N> variant structure (1..2)
|
|
||||||
```
|
|
||||||
|
|
||||||
Для DataSetUnion — дерево наборов + связи:
|
|
||||||
```
|
|
||||||
Datasets:
|
|
||||||
[Union] РасчетНалогаНаИмущество 52 fields
|
|
||||||
├─ [Query] РасчетНалогаНаИмущество 51 fields, query 181 lines
|
|
||||||
├─ [Query] ДанныеПоКадастровой 29 fields, query 40 lines
|
|
||||||
├─ [Query] ДанныеПоСреднегодовой 34 fields, query 41 lines
|
|
||||||
Links: РасчетНалогаНаИмущество -> СостояниеОС (2 fields)
|
|
||||||
```
|
|
||||||
|
|
||||||
Параметры разделяются на видимые/скрытые:
|
|
||||||
```
|
|
||||||
Params: 18 (7 visible, 11 hidden): Период, Ответственный, ...
|
|
||||||
```
|
|
||||||
|
|
||||||
## query — текст запроса
|
|
||||||
|
|
||||||
`-Name <набор>` — имя DataSet (обязателен если наборов > 1).
|
|
||||||
|
|
||||||
Извлекает raw-текст запроса с деэкранированием XML (`&`→`&`, `>`→`>`). Для пакетных запросов — оглавление батчей:
|
|
||||||
|
|
||||||
```
|
|
||||||
=== Query: ДанныеТ13 (334 lines, 13 batches) ===
|
|
||||||
Batch 1: lines 1-8 → ПОМЕСТИТЬ Представления_Периоды
|
|
||||||
Batch 2: lines 9-26 → ПОМЕСТИТЬ Представления_СотрудникиОрганизации
|
|
||||||
...
|
|
||||||
--- Batch 1 ---
|
|
||||||
ВЫБРАТЬ
|
|
||||||
ДАТАВРЕМЯ(1, 1, 1) КАК Период
|
|
||||||
ПОМЕСТИТЬ Представления_Периоды
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
Фильтр по номеру батча: `-Batch 3` покажет только 3-й пакет.
|
|
||||||
|
|
||||||
## fields — поля наборов данных
|
|
||||||
|
|
||||||
Без `-Name` — карта: имена полей по наборам:
|
|
||||||
```
|
|
||||||
=== Fields map ===
|
|
||||||
СостояниеОС [Query] (3): Организация, ОсновноеСредство, ДатаСостояния
|
|
||||||
РасчетНалогаНаИмущество [Union] (52): ДоляСтоимостиЧислитель, ...
|
|
||||||
РасчетНалогаНаИмущество [Query] (51): КадастроваяСтоимость, ...
|
|
||||||
```
|
|
||||||
|
|
||||||
С `-Name <поле>` — детали конкретного поля:
|
|
||||||
```
|
|
||||||
=== Field: ДатаСостояния "Дата ввода в эксплуатацию" ===
|
|
||||||
|
|
||||||
Dataset: СостояниеОС [Query]
|
|
||||||
Format: ДФ=dd.MM.yyyy
|
|
||||||
```
|
|
||||||
|
|
||||||
Показывает: dataset, title, type, role, useRestriction, format, presentationExpression.
|
|
||||||
|
|
||||||
## links — связи наборов данных
|
|
||||||
|
|
||||||
```
|
|
||||||
=== Links (4) ===
|
|
||||||
|
|
||||||
РасчетНалогаНаИмущество -> СостояниеОС :
|
|
||||||
Организация -> Организация
|
|
||||||
ОсновноеСредство -> ОсновноеСредство
|
|
||||||
```
|
|
||||||
|
|
||||||
Группирует по парам наборов. Показывает поля связи и параметры.
|
|
||||||
|
|
||||||
## calculated — вычисляемые поля
|
|
||||||
|
|
||||||
Без `-Name` — карта: имена и заголовки:
|
|
||||||
```
|
|
||||||
=== Calculated fields (23) ===
|
|
||||||
ДоляСтоимости "Доля стоимости"
|
|
||||||
КоэффициентКи "Коэффициент Ки"
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
С `-Name <поле>` — полное выражение:
|
|
||||||
```
|
|
||||||
=== Calculated: ДоляСтоимости ===
|
|
||||||
|
|
||||||
Expression:
|
|
||||||
ВЫБОР КОГДА ... ТОГДА "1" ИНАЧЕ ... КОНЕЦ
|
|
||||||
Title: Доля стоимости
|
|
||||||
Restrict: condition
|
|
||||||
```
|
|
||||||
|
|
||||||
## resources — ресурсы (итоги по группировкам)
|
|
||||||
|
|
||||||
Без `-Name` — карта: имена полей, `*` = есть формулы по группировкам:
|
|
||||||
```
|
|
||||||
=== Resources (51) ===
|
|
||||||
НалоговаяБаза
|
|
||||||
КоэффициентКи *
|
|
||||||
...
|
|
||||||
* = has group-level formulas
|
|
||||||
```
|
|
||||||
|
|
||||||
С `-Name <поле>` — формулы агрегации:
|
|
||||||
```
|
|
||||||
=== Resource: ДатаСостояния ===
|
|
||||||
|
|
||||||
[ОсновноеСредство] ЕстьNull(ДатаСостояния, "")
|
|
||||||
```
|
|
||||||
|
|
||||||
## params — параметры схемы
|
|
||||||
|
|
||||||
```
|
|
||||||
=== Parameters (16) ===
|
|
||||||
Name Type Default Visible Expression
|
|
||||||
Период StandardPeriod LastMonth yes -
|
|
||||||
НачалоПериода DateTime - hidden &Период.ДатаНачала
|
|
||||||
Организация CatalogRef.Организации null yes -
|
|
||||||
```
|
|
||||||
|
|
||||||
## variant — варианты отчёта
|
|
||||||
|
|
||||||
Без `-Name` — список вариантов:
|
|
||||||
```
|
|
||||||
=== Variants (2) ===
|
|
||||||
[1] НоменклатураИЦены "Номенклатура и цены" Table(detail) 3 filters
|
|
||||||
[2] НоменклатураБезЦен "Номенклатура без цен" Group(detail) 2 filters
|
|
||||||
```
|
|
||||||
|
|
||||||
С `-Name <N|имя>` — структура конкретного варианта:
|
|
||||||
```
|
|
||||||
=== Variant [1]: НоменклатураИЦены "Номенклатура и цены" ===
|
|
||||||
|
|
||||||
Structure:
|
|
||||||
Table "Таблица"
|
|
||||||
├── Columns: [ТипЦен Items]
|
|
||||||
│ Selection: Auto, Цена
|
|
||||||
└── Rows: [Номенклатура Items]
|
|
||||||
Selection: Номенклатура, УИД, Auto
|
|
||||||
|
|
||||||
Filter:
|
|
||||||
[ ] Номенклатура InHierarchy [user]
|
|
||||||
[ ] ТипЦен Equal
|
|
||||||
[x] ВАрхиве = false "Исключая скрытые товары"
|
|
||||||
|
|
||||||
DataParams: КлючВарианта="НоменклатураИЦены"
|
|
||||||
Output: style=ЧерноБелый groups=Separately totalsH=None totalsV=None
|
|
||||||
```
|
|
||||||
|
|
||||||
## templates — привязки шаблонов вывода
|
|
||||||
|
|
||||||
Три типа привязок: `fieldTemplate` (к полю), `groupTemplate` (к группировке, Header/Footer), `groupHeaderTemplate` (заголовок группы).
|
|
||||||
|
|
||||||
Без `-Name` — карта привязок:
|
|
||||||
```
|
|
||||||
=== Templates (70 defined: 49 field, 37 group) ===
|
|
||||||
|
|
||||||
Field bindings (49): (all trivial)
|
|
||||||
ОстаточнаяСтоимостьНа0101, ОстаточнаяСтоимостьНа0102, ...
|
|
||||||
|
|
||||||
Group bindings (37):
|
|
||||||
ВидНалоговойБазы
|
|
||||||
Header -> Макет3 (1 rows, 1 params)
|
|
||||||
СреднегодоваяСтоимость2019
|
|
||||||
Footer -> Макет50 (1 rows) spacer
|
|
||||||
GroupHeader -> Макет40 (3 rows)
|
|
||||||
```
|
|
||||||
|
|
||||||
С `-Name <группировка|поле>` — содержимое шаблонов:
|
|
||||||
```
|
|
||||||
=== Templates: СреднегодоваяСтоимость2019 ===
|
|
||||||
|
|
||||||
Footer -> Макет50 [1 rows, 1 cells]:
|
|
||||||
Row 1: (empty)
|
|
||||||
|
|
||||||
GroupHeader -> Макет40 [3 rows, 78 cells]:
|
|
||||||
Row 1: "№ п/п" | "###Группировки1###" | "Инв. номер" | ...
|
|
||||||
Row 2: "01.01" | "01.02" | ... | "31.12"
|
|
||||||
Row 3: "1" | "2" | ... | "26"
|
|
||||||
```
|
|
||||||
|
|
||||||
Для field-привязок:
|
|
||||||
```
|
|
||||||
=== Field template: ОстаточнаяСтоимостьНа0101 -> Макет4 ===
|
|
||||||
[1 rows, 1 cells]
|
|
||||||
Row 1: {ОстаточнаяСтоимостьНа0101}
|
|
||||||
(all params trivial)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Тривиальность выражений**: `Поле = Поле` и `Поле = Представление(Поле)` считаются тривиальными и НЕ выводятся. Показываются только нетривиальные — когда выражение содержит другое поле, вызов метода, пустую строку и т.д.
|
|
||||||
|
|
||||||
## trace — трассировка поля от заголовка до запроса
|
|
||||||
|
|
||||||
Ищет поле по dataPath ИЛИ заголовку (включая подстроку) и показывает полную цепочку происхождения за один вызов:
|
|
||||||
|
|
||||||
```
|
|
||||||
=== Trace: КоэффициентКи "Коэффициент Ки" ===
|
|
||||||
|
|
||||||
Dataset: (schema-level only, not in dataset fields)
|
|
||||||
|
|
||||||
Calculated:
|
|
||||||
ВЫБОР КОГДА ... ТОГДА 0 ИНАЧЕ ... КОНЕЦ
|
|
||||||
Operands:
|
|
||||||
КоличествоМесяцевИспользования -> РасчетНалогаНаИмущество [Query]
|
|
||||||
КоличествоМесяцевВладения -> РасчетНалогаНаИмущество [Query]
|
|
||||||
|
|
||||||
Resource:
|
|
||||||
[ОсновноеСредство] Сумма(КоэффициентКи)
|
|
||||||
```
|
|
||||||
|
|
||||||
Типичный сценарий: пользователь видит колонку "Коэффициент Ки" в отчёте и спрашивает как она считается. Один вызов `trace` показывает: формулу вычисления, откуда берутся операнды, как агрегируется в ресурс.
|
|
||||||
|
|
||||||
## Что не выводится
|
|
||||||
|
|
||||||
- XML namespace-декларации
|
|
||||||
- Обёртки v8:item/v8:lang/v8:content (извлекаем чистый текст)
|
|
||||||
- userSettingID (GUID-ы пользовательских настроек)
|
|
||||||
- Дефолтные periodAdditionBegin/End = 0001-01-01
|
|
||||||
- viewMode
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
---
|
|
||||||
name: skd-validate
|
|
||||||
description: Валидация схемы компоновки данных 1С (СКД). Используй после создания или модификации СКД для проверки корректности
|
|
||||||
argument-hint: <TemplatePath> [-Detailed] [-MaxErrors 20]
|
|
||||||
allowed-tools:
|
|
||||||
- Bash
|
|
||||||
- Read
|
|
||||||
- Glob
|
|
||||||
---
|
|
||||||
|
|
||||||
# /skd-validate — валидация СКД (DataCompositionSchema)
|
|
||||||
|
|
||||||
Проверяет структурную корректность Template.xml схемы компоновки данных. Выявляет ошибки формата, битые ссылки, дубликаты имён.
|
|
||||||
|
|
||||||
## Параметры
|
|
||||||
|
|
||||||
| Параметр | Обяз. | Умолч. | Описание |
|
|
||||||
|--------------|:-----:|---------|---------------------------------------------------------|
|
|
||||||
| TemplatePath | да | — | Путь к Template.xml или каталогу макета |
|
|
||||||
| Detailed | нет | — | Показывать [OK] для каждой проверки |
|
|
||||||
| MaxErrors | нет | 20 | Остановиться после N ошибок |
|
|
||||||
| OutFile | нет | — | Записать результат в файл |
|
|
||||||
|
|
||||||
## Команда
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
powershell.exe -NoProfile -File .claude/skills/skd-validate/scripts/skd-validate.ps1 -TemplatePath "src/МойОтчёт/Templates/ОсновнаяСхема"
|
|
||||||
powershell.exe -NoProfile -File .claude/skills/skd-validate/scripts/skd-validate.ps1 -TemplatePath "Catalogs/Номенклатура/Templates/СКД/Ext/Template.xml"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Проверки (~30)
|
|
||||||
|
|
||||||
| Группа | Что проверяется |
|
|
||||||
|--------|-----------------|
|
|
||||||
| **Root** | XML parse, корневой элемент `DataCompositionSchema`, default namespace, ns-префиксы |
|
|
||||||
| **DataSource** | Наличие, name не пуст, type валиден (Local/External), уникальность имён |
|
|
||||||
| **DataSet** | Наличие, xsi:type валиден, name не пуст, уникальность, ссылка на dataSource, query не пуст |
|
|
||||||
| **Fields** | dataPath не пуст, field не пуст, уникальность dataPath в наборе |
|
|
||||||
| **Links** | source/dest ссылаются на существующие наборы, expressions не пусты |
|
|
||||||
| **CalcFields** | dataPath не пуст, expression не пуст, уникальность, коллизии с полями наборов |
|
|
||||||
| **TotalFields** | dataPath не пуст, expression не пуст |
|
|
||||||
| **Parameters** | name не пуст, уникальность |
|
|
||||||
| **Templates** | name не пуст, уникальность |
|
|
||||||
| **GroupTemplates** | template ссылается на существующий template, templateType валиден |
|
|
||||||
| **Variants** | Наличие, name не пуст, settings element присутствует |
|
|
||||||
| **Settings** | selection/filter/order ссылаются на известные поля, comparisonType валиден, structure items типизированы |
|
|
||||||
|
|
||||||
Exit code: 0 = OK, 1 = есть ошибки. По умолчанию краткий вывод. `-Detailed` для поштучной детализации.
|
|
||||||
@@ -1,338 +0,0 @@
|
|||||||
# subsystem-compile v1.0 — Create 1C subsystem from JSON definition
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
param(
|
|
||||||
[string]$DefinitionFile,
|
|
||||||
[string]$Value,
|
|
||||||
[Parameter(Mandatory)][string]$OutputDir,
|
|
||||||
[string]$Parent,
|
|
||||||
[switch]$NoValidate
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
|
|
||||||
# --- 1. Load JSON ---
|
|
||||||
if ($DefinitionFile -and $Value) {
|
|
||||||
Write-Error "Cannot use both -DefinitionFile and -Value"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
if (-not $DefinitionFile -and -not $Value) {
|
|
||||||
Write-Error "Either -DefinitionFile or -Value is required"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($DefinitionFile) {
|
|
||||||
if (-not [System.IO.Path]::IsPathRooted($DefinitionFile)) {
|
|
||||||
$DefinitionFile = Join-Path (Get-Location).Path $DefinitionFile
|
|
||||||
}
|
|
||||||
if (-not (Test-Path $DefinitionFile)) {
|
|
||||||
Write-Error "Definition file not found: $DefinitionFile"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
$json = Get-Content -Raw -Encoding UTF8 $DefinitionFile
|
|
||||||
} else {
|
|
||||||
$json = $Value
|
|
||||||
}
|
|
||||||
|
|
||||||
$def = $json | ConvertFrom-Json
|
|
||||||
|
|
||||||
if (-not $def.name) {
|
|
||||||
Write-Error "JSON must have 'name' field"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
$objName = "$($def.name)"
|
|
||||||
|
|
||||||
# Resolve OutputDir
|
|
||||||
if (-not [System.IO.Path]::IsPathRooted($OutputDir)) {
|
|
||||||
$OutputDir = Join-Path (Get-Location).Path $OutputDir
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 2. XML helpers ---
|
|
||||||
$script:xml = New-Object System.Text.StringBuilder 8192
|
|
||||||
|
|
||||||
function X([string]$text) {
|
|
||||||
$script:xml.AppendLine($text) | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
function Esc-Xml([string]$s) {
|
|
||||||
return $s.Replace('&','&').Replace('<','<').Replace('>','>').Replace('"','"')
|
|
||||||
}
|
|
||||||
|
|
||||||
function Split-CamelCase([string]$name) {
|
|
||||||
if (-not $name) { return $name }
|
|
||||||
$result = [regex]::Replace($name, '([a-z\u0430-\u044F\u0451])([A-Z\u0410-\u042F\u0401])', '$1 $2')
|
|
||||||
if ($result.Length -gt 1) {
|
|
||||||
$result = $result.Substring(0,1) + $result.Substring(1).ToLower()
|
|
||||||
}
|
|
||||||
return $result
|
|
||||||
}
|
|
||||||
|
|
||||||
function Emit-MLText([string]$indent, [string]$tag, [string]$text) {
|
|
||||||
if (-not $text) {
|
|
||||||
X "$indent<$tag/>"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
X "$indent<$tag>"
|
|
||||||
X "$indent`t<v8:item>"
|
|
||||||
X "$indent`t`t<v8:lang>ru</v8:lang>"
|
|
||||||
X "$indent`t`t<v8:content>$(Esc-Xml $text)</v8:content>"
|
|
||||||
X "$indent`t</v8:item>"
|
|
||||||
X "$indent</$tag>"
|
|
||||||
}
|
|
||||||
|
|
||||||
function New-Guid-String {
|
|
||||||
return [System.Guid]::NewGuid().ToString()
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 3. Resolve defaults ---
|
|
||||||
$synonym = if ($def.synonym) { "$($def.synonym)" } else { Split-CamelCase $objName }
|
|
||||||
$comment = if ($def.comment) { "$($def.comment)" } else { "" }
|
|
||||||
$includeHelpInContents = "true"
|
|
||||||
$includeInCI = if ($null -ne $def.includeInCommandInterface) { "$($def.includeInCommandInterface)".ToLower() } else { "true" }
|
|
||||||
$useOneCommand = if ($null -ne $def.useOneCommand) { "$($def.useOneCommand)".ToLower() } else { "false" }
|
|
||||||
$explanation = if ($def.explanation) { "$($def.explanation)" } else { "" }
|
|
||||||
$picture = if ($def.picture) { "$($def.picture)" } else { "" }
|
|
||||||
|
|
||||||
$contentItems = @()
|
|
||||||
if ($def.content) {
|
|
||||||
foreach ($c in $def.content) { $contentItems += "$c" }
|
|
||||||
}
|
|
||||||
|
|
||||||
$children = @()
|
|
||||||
if ($def.children) {
|
|
||||||
foreach ($ch in $def.children) { $children += "$ch" }
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 4. Build XML ---
|
|
||||||
$uuid = New-Guid-String
|
|
||||||
$indent = "`t`t`t"
|
|
||||||
|
|
||||||
X '<?xml version="1.0" encoding="UTF-8"?>'
|
|
||||||
X '<MetaDataObject 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" version="2.17">'
|
|
||||||
X "`t<Subsystem uuid=`"$uuid`">"
|
|
||||||
X "`t`t<Properties>"
|
|
||||||
|
|
||||||
# Name
|
|
||||||
X "`t`t`t<Name>$(Esc-Xml $objName)</Name>"
|
|
||||||
|
|
||||||
# Synonym
|
|
||||||
Emit-MLText "`t`t`t" "Synonym" $synonym
|
|
||||||
|
|
||||||
# Comment
|
|
||||||
if ($comment) {
|
|
||||||
X "`t`t`t<Comment>$(Esc-Xml $comment)</Comment>"
|
|
||||||
} else {
|
|
||||||
X "`t`t`t<Comment/>"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Boolean properties
|
|
||||||
X "`t`t`t<IncludeHelpInContents>$includeHelpInContents</IncludeHelpInContents>"
|
|
||||||
X "`t`t`t<IncludeInCommandInterface>$includeInCI</IncludeInCommandInterface>"
|
|
||||||
X "`t`t`t<UseOneCommand>$useOneCommand</UseOneCommand>"
|
|
||||||
|
|
||||||
# Explanation
|
|
||||||
Emit-MLText "`t`t`t" "Explanation" $explanation
|
|
||||||
|
|
||||||
# Picture
|
|
||||||
if ($picture) {
|
|
||||||
X "`t`t`t<Picture>"
|
|
||||||
X "`t`t`t`t<xr:Ref>$picture</xr:Ref>"
|
|
||||||
X "`t`t`t`t<xr:LoadTransparent>false</xr:LoadTransparent>"
|
|
||||||
X "`t`t`t</Picture>"
|
|
||||||
} else {
|
|
||||||
X "`t`t`t<Picture/>"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Content
|
|
||||||
if ($contentItems.Count -gt 0) {
|
|
||||||
X "`t`t`t<Content>"
|
|
||||||
foreach ($item in $contentItems) {
|
|
||||||
X "`t`t`t`t<xr:Item xsi:type=`"xr:MDObjectRef`">$(Esc-Xml $item)</xr:Item>"
|
|
||||||
}
|
|
||||||
X "`t`t`t</Content>"
|
|
||||||
} else {
|
|
||||||
X "`t`t`t<Content/>"
|
|
||||||
}
|
|
||||||
|
|
||||||
X "`t`t</Properties>"
|
|
||||||
|
|
||||||
# ChildObjects
|
|
||||||
if ($children.Count -gt 0) {
|
|
||||||
X "`t`t<ChildObjects>"
|
|
||||||
foreach ($ch in $children) {
|
|
||||||
X "`t`t`t<Subsystem>$(Esc-Xml $ch)</Subsystem>"
|
|
||||||
}
|
|
||||||
X "`t`t</ChildObjects>"
|
|
||||||
} else {
|
|
||||||
X "`t`t<ChildObjects/>"
|
|
||||||
}
|
|
||||||
|
|
||||||
X "`t</Subsystem>"
|
|
||||||
X '</MetaDataObject>'
|
|
||||||
|
|
||||||
# --- 5. Write files ---
|
|
||||||
|
|
||||||
# Determine target directory
|
|
||||||
if ($Parent) {
|
|
||||||
# Nested subsystem
|
|
||||||
if (-not [System.IO.Path]::IsPathRooted($Parent)) {
|
|
||||||
$Parent = Join-Path (Get-Location).Path $Parent
|
|
||||||
}
|
|
||||||
if (-not (Test-Path $Parent)) {
|
|
||||||
Write-Error "Parent subsystem not found: $Parent"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
$parentDir = [System.IO.Path]::GetDirectoryName($Parent)
|
|
||||||
$parentBaseName = [System.IO.Path]::GetFileNameWithoutExtension($Parent)
|
|
||||||
$subsDir = Join-Path (Join-Path $parentDir $parentBaseName) "Subsystems"
|
|
||||||
} else {
|
|
||||||
# Top-level subsystem
|
|
||||||
$subsDir = Join-Path $OutputDir "Subsystems"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Test-Path $subsDir)) {
|
|
||||||
New-Item -ItemType Directory -Path $subsDir -Force | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
$targetXml = Join-Path $subsDir "$objName.xml"
|
|
||||||
|
|
||||||
# Write XML
|
|
||||||
$xmlContent = $script:xml.ToString()
|
|
||||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
|
||||||
[System.IO.File]::WriteAllText($targetXml, $xmlContent, $utf8Bom)
|
|
||||||
Write-Host "[OK] Created: $targetXml"
|
|
||||||
|
|
||||||
# Create subdirectory if children exist
|
|
||||||
if ($children.Count -gt 0) {
|
|
||||||
$childSubsDir = Join-Path (Join-Path $subsDir $objName) "Subsystems"
|
|
||||||
if (-not (Test-Path $childSubsDir)) {
|
|
||||||
New-Item -ItemType Directory -Path $childSubsDir -Force | Out-Null
|
|
||||||
Write-Host "[OK] Created directory: $childSubsDir"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 6. Register in parent ---
|
|
||||||
$parentXmlPath = $null
|
|
||||||
if ($Parent) {
|
|
||||||
$parentXmlPath = $Parent
|
|
||||||
} else {
|
|
||||||
$configXml = Join-Path $OutputDir "Configuration.xml"
|
|
||||||
if (Test-Path $configXml) {
|
|
||||||
$parentXmlPath = $configXml
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($parentXmlPath -and (Test-Path $parentXmlPath)) {
|
|
||||||
$doc = New-Object System.Xml.XmlDocument
|
|
||||||
$doc.PreserveWhitespace = $true
|
|
||||||
$doc.Load($parentXmlPath)
|
|
||||||
|
|
||||||
$ns = New-Object System.Xml.XmlNamespaceManager($doc.NameTable)
|
|
||||||
$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
|
||||||
|
|
||||||
# Find ChildObjects
|
|
||||||
$childObjects = $null
|
|
||||||
if ($Parent) {
|
|
||||||
$childObjects = $doc.SelectSingleNode("//md:Subsystem/md:ChildObjects", $ns)
|
|
||||||
} else {
|
|
||||||
$childObjects = $doc.SelectSingleNode("//md:Configuration/md:ChildObjects", $ns)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($childObjects) {
|
|
||||||
# Check for self-closing tag
|
|
||||||
$isSelfClosing = (-not $childObjects.HasChildNodes) -or ($childObjects.IsEmpty)
|
|
||||||
|
|
||||||
# Check if already registered
|
|
||||||
$alreadyExists = $false
|
|
||||||
foreach ($child in $childObjects.ChildNodes) {
|
|
||||||
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Subsystem" -and $child.InnerText -eq $objName) {
|
|
||||||
$alreadyExists = $true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $alreadyExists) {
|
|
||||||
$newEl = $doc.CreateElement("Subsystem", "http://v8.1c.ru/8.3/MDClasses")
|
|
||||||
$newEl.InnerText = $objName
|
|
||||||
|
|
||||||
if ($isSelfClosing) {
|
|
||||||
# Expand self-closing tag
|
|
||||||
$parentIndent = ""
|
|
||||||
$prev = $childObjects.PreviousSibling
|
|
||||||
if ($prev -and ($prev.NodeType -eq 'Whitespace' -or $prev.NodeType -eq 'SignificantWhitespace')) {
|
|
||||||
if ($prev.Value -match '(\t+)$') { $parentIndent = $Matches[1] }
|
|
||||||
}
|
|
||||||
$childIndent = "$parentIndent`t"
|
|
||||||
$ws1 = $doc.CreateWhitespace("`r`n$childIndent")
|
|
||||||
$ws2 = $doc.CreateWhitespace("`r`n$parentIndent")
|
|
||||||
$childObjects.AppendChild($ws1) | Out-Null
|
|
||||||
$childObjects.AppendChild($newEl) | Out-Null
|
|
||||||
$childObjects.AppendChild($ws2) | Out-Null
|
|
||||||
} else {
|
|
||||||
# Insert before trailing whitespace
|
|
||||||
$childIndent = "`t`t`t"
|
|
||||||
foreach ($child in $childObjects.ChildNodes) {
|
|
||||||
if ($child.NodeType -eq 'Whitespace' -or $child.NodeType -eq 'SignificantWhitespace') {
|
|
||||||
if ($child.Value -match '^\r?\n(\t+)') { $childIndent = $Matches[1]; break }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$trailing = $childObjects.LastChild
|
|
||||||
$ws = $doc.CreateWhitespace("`r`n$childIndent")
|
|
||||||
if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) {
|
|
||||||
$childObjects.InsertBefore($ws, $trailing) | Out-Null
|
|
||||||
$childObjects.InsertBefore($newEl, $trailing) | Out-Null
|
|
||||||
} else {
|
|
||||||
$childObjects.AppendChild($ws) | Out-Null
|
|
||||||
$childObjects.AppendChild($newEl) | Out-Null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Save parent 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)
|
|
||||||
$doc.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"')
|
|
||||||
[System.IO.File]::WriteAllText($parentXmlPath, $text, $utf8Bom)
|
|
||||||
|
|
||||||
Write-Host "[OK] Registered in: $parentXmlPath"
|
|
||||||
} else {
|
|
||||||
Write-Host "[SKIP] Already registered in: $parentXmlPath"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Write-Host "[WARN] ChildObjects not found in: $parentXmlPath"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Write-Host "[INFO] No parent XML to register in"
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 7. Auto-validate ---
|
|
||||||
if (-not $NoValidate) {
|
|
||||||
$validateScript = Join-Path (Join-Path $PSScriptRoot "..\..\subsystem-validate") "scripts\subsystem-validate.ps1"
|
|
||||||
$validateScript = [System.IO.Path]::GetFullPath($validateScript)
|
|
||||||
if (Test-Path $validateScript) {
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "--- Running subsystem-validate ---"
|
|
||||||
& powershell.exe -NoProfile -File $validateScript -SubsystemPath $targetXml
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "=== subsystem-compile summary ==="
|
|
||||||
Write-Host " Name: $objName"
|
|
||||||
Write-Host " UUID: $uuid"
|
|
||||||
Write-Host " Content: $($contentItems.Count) objects"
|
|
||||||
Write-Host " Children: $($children.Count)"
|
|
||||||
Write-Host " File: $targetXml"
|
|
||||||
exit 0
|
|
||||||
@@ -1,288 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# subsystem-compile v1.0 — Create 1C subsystem from JSON definition
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
import uuid
|
|
||||||
import xml.etree.ElementTree as ET
|
|
||||||
|
|
||||||
|
|
||||||
def esc_xml(s):
|
|
||||||
return s.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"')
|
|
||||||
|
|
||||||
|
|
||||||
def emit_mltext(lines, indent, tag, text):
|
|
||||||
if not text:
|
|
||||||
lines.append(f"{indent}<{tag}/>")
|
|
||||||
return
|
|
||||||
lines.append(f"{indent}<{tag}>")
|
|
||||||
lines.append(f"{indent}\t<v8:item>")
|
|
||||||
lines.append(f"{indent}\t\t<v8:lang>ru</v8:lang>")
|
|
||||||
lines.append(f"{indent}\t\t<v8:content>{esc_xml(text)}</v8:content>")
|
|
||||||
lines.append(f"{indent}\t</v8:item>")
|
|
||||||
lines.append(f"{indent}</{tag}>")
|
|
||||||
|
|
||||||
|
|
||||||
def new_uuid():
|
|
||||||
return str(uuid.uuid4())
|
|
||||||
|
|
||||||
|
|
||||||
def write_utf8_bom(path, content):
|
|
||||||
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
|
|
||||||
f.write(content)
|
|
||||||
|
|
||||||
|
|
||||||
def split_camel_case(name):
|
|
||||||
if not name:
|
|
||||||
return name
|
|
||||||
result = re.sub(r'([a-z\u0430-\u044f\u0451])([A-Z\u0410-\u042f\u0401])', r'\1 \2', name)
|
|
||||||
if len(result) > 1:
|
|
||||||
result = result[0] + result[1:].lower()
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(description='Compile 1C subsystem from JSON definition', allow_abbrev=False)
|
|
||||||
parser.add_argument('-DefinitionFile', type=str, default=None)
|
|
||||||
parser.add_argument('-Value', type=str, default=None)
|
|
||||||
parser.add_argument('-OutputDir', type=str, required=True)
|
|
||||||
parser.add_argument('-Parent', type=str, default=None)
|
|
||||||
parser.add_argument('-NoValidate', action='store_true', default=False)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
# --- 1. Load JSON ---
|
|
||||||
if args.DefinitionFile and args.Value:
|
|
||||||
print("Cannot use both -DefinitionFile and -Value", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
if not args.DefinitionFile and not args.Value:
|
|
||||||
print("Either -DefinitionFile or -Value is required", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if args.DefinitionFile:
|
|
||||||
def_file = args.DefinitionFile
|
|
||||||
if not os.path.isabs(def_file):
|
|
||||||
def_file = os.path.join(os.getcwd(), def_file)
|
|
||||||
if not os.path.exists(def_file):
|
|
||||||
print(f"Definition file not found: {def_file}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
with open(def_file, 'r', encoding='utf-8-sig') as f:
|
|
||||||
json_text = f.read()
|
|
||||||
else:
|
|
||||||
json_text = args.Value
|
|
||||||
|
|
||||||
defn = json.loads(json_text)
|
|
||||||
|
|
||||||
if not defn.get('name'):
|
|
||||||
print("JSON must have 'name' field", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
obj_name = str(defn['name'])
|
|
||||||
|
|
||||||
# Resolve OutputDir
|
|
||||||
output_dir = args.OutputDir
|
|
||||||
if not os.path.isabs(output_dir):
|
|
||||||
output_dir = os.path.join(os.getcwd(), output_dir)
|
|
||||||
|
|
||||||
# --- 2. Resolve defaults ---
|
|
||||||
synonym = str(defn['synonym']) if defn.get('synonym') else split_camel_case(obj_name)
|
|
||||||
comment = str(defn['comment']) if defn.get('comment') else ''
|
|
||||||
include_help_in_contents = 'true'
|
|
||||||
include_in_ci = str(defn['includeInCommandInterface']).lower() if defn.get('includeInCommandInterface') is not None else 'true'
|
|
||||||
use_one_command = str(defn['useOneCommand']).lower() if defn.get('useOneCommand') is not None else 'false'
|
|
||||||
explanation = str(defn['explanation']) if defn.get('explanation') else ''
|
|
||||||
picture = str(defn['picture']) if defn.get('picture') else ''
|
|
||||||
|
|
||||||
content_items = []
|
|
||||||
if defn.get('content'):
|
|
||||||
for c in defn['content']:
|
|
||||||
content_items.append(str(c))
|
|
||||||
|
|
||||||
children = []
|
|
||||||
if defn.get('children'):
|
|
||||||
for ch in defn['children']:
|
|
||||||
children.append(str(ch))
|
|
||||||
|
|
||||||
# --- 3. Build XML ---
|
|
||||||
uid = new_uuid()
|
|
||||||
lines = []
|
|
||||||
|
|
||||||
lines.append('<?xml version="1.0" encoding="UTF-8"?>')
|
|
||||||
lines.append('<MetaDataObject 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" version="2.17">')
|
|
||||||
lines.append(f'\t<Subsystem uuid="{uid}">')
|
|
||||||
lines.append('\t\t<Properties>')
|
|
||||||
|
|
||||||
# Name
|
|
||||||
lines.append(f'\t\t\t<Name>{esc_xml(obj_name)}</Name>')
|
|
||||||
|
|
||||||
# Synonym
|
|
||||||
emit_mltext(lines, '\t\t\t', 'Synonym', synonym)
|
|
||||||
|
|
||||||
# Comment
|
|
||||||
if comment:
|
|
||||||
lines.append(f'\t\t\t<Comment>{esc_xml(comment)}</Comment>')
|
|
||||||
else:
|
|
||||||
lines.append('\t\t\t<Comment/>')
|
|
||||||
|
|
||||||
# Boolean properties
|
|
||||||
lines.append(f'\t\t\t<IncludeHelpInContents>{include_help_in_contents}</IncludeHelpInContents>')
|
|
||||||
lines.append(f'\t\t\t<IncludeInCommandInterface>{include_in_ci}</IncludeInCommandInterface>')
|
|
||||||
lines.append(f'\t\t\t<UseOneCommand>{use_one_command}</UseOneCommand>')
|
|
||||||
|
|
||||||
# Explanation
|
|
||||||
emit_mltext(lines, '\t\t\t', 'Explanation', explanation)
|
|
||||||
|
|
||||||
# Picture
|
|
||||||
if picture:
|
|
||||||
lines.append('\t\t\t<Picture>')
|
|
||||||
lines.append(f'\t\t\t\t<xr:Ref>{picture}</xr:Ref>')
|
|
||||||
lines.append('\t\t\t\t<xr:LoadTransparent>false</xr:LoadTransparent>')
|
|
||||||
lines.append('\t\t\t</Picture>')
|
|
||||||
else:
|
|
||||||
lines.append('\t\t\t<Picture/>')
|
|
||||||
|
|
||||||
# Content
|
|
||||||
if len(content_items) > 0:
|
|
||||||
lines.append('\t\t\t<Content>')
|
|
||||||
for item in content_items:
|
|
||||||
lines.append(f'\t\t\t\t<xr:Item xsi:type="xr:MDObjectRef">{esc_xml(item)}</xr:Item>')
|
|
||||||
lines.append('\t\t\t</Content>')
|
|
||||||
else:
|
|
||||||
lines.append('\t\t\t<Content/>')
|
|
||||||
|
|
||||||
lines.append('\t\t</Properties>')
|
|
||||||
|
|
||||||
# ChildObjects
|
|
||||||
if len(children) > 0:
|
|
||||||
lines.append('\t\t<ChildObjects>')
|
|
||||||
for ch in children:
|
|
||||||
lines.append(f'\t\t\t<Subsystem>{esc_xml(ch)}</Subsystem>')
|
|
||||||
lines.append('\t\t</ChildObjects>')
|
|
||||||
else:
|
|
||||||
lines.append('\t\t<ChildObjects/>')
|
|
||||||
|
|
||||||
lines.append('\t</Subsystem>')
|
|
||||||
lines.append('</MetaDataObject>')
|
|
||||||
|
|
||||||
# --- 4. Write files ---
|
|
||||||
parent = args.Parent
|
|
||||||
|
|
||||||
if parent:
|
|
||||||
# Nested subsystem
|
|
||||||
if not os.path.isabs(parent):
|
|
||||||
parent = os.path.join(os.getcwd(), parent)
|
|
||||||
if not os.path.exists(parent):
|
|
||||||
print(f"Parent subsystem not found: {parent}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
parent_dir = os.path.dirname(parent)
|
|
||||||
parent_base_name = os.path.splitext(os.path.basename(parent))[0]
|
|
||||||
subs_dir = os.path.join(parent_dir, parent_base_name, 'Subsystems')
|
|
||||||
else:
|
|
||||||
# Top-level subsystem
|
|
||||||
subs_dir = os.path.join(output_dir, 'Subsystems')
|
|
||||||
|
|
||||||
os.makedirs(subs_dir, exist_ok=True)
|
|
||||||
|
|
||||||
target_xml = os.path.join(subs_dir, f'{obj_name}.xml')
|
|
||||||
|
|
||||||
# Write XML
|
|
||||||
xml_content = '\n'.join(lines) + '\n'
|
|
||||||
write_utf8_bom(target_xml, xml_content)
|
|
||||||
print(f"[OK] Created: {target_xml}")
|
|
||||||
|
|
||||||
# Create subdirectory if children exist
|
|
||||||
if len(children) > 0:
|
|
||||||
child_subs_dir = os.path.join(subs_dir, obj_name, 'Subsystems')
|
|
||||||
if not os.path.exists(child_subs_dir):
|
|
||||||
os.makedirs(child_subs_dir, exist_ok=True)
|
|
||||||
print(f"[OK] Created directory: {child_subs_dir}")
|
|
||||||
|
|
||||||
# --- 5. Register in parent ---
|
|
||||||
parent_xml_path = None
|
|
||||||
if parent:
|
|
||||||
parent_xml_path = parent
|
|
||||||
else:
|
|
||||||
config_xml = os.path.join(output_dir, 'Configuration.xml')
|
|
||||||
if os.path.exists(config_xml):
|
|
||||||
parent_xml_path = config_xml
|
|
||||||
|
|
||||||
if parent_xml_path and os.path.exists(parent_xml_path):
|
|
||||||
with open(parent_xml_path, 'r', encoding='utf-8-sig') as f:
|
|
||||||
raw_text = f.read()
|
|
||||||
|
|
||||||
doc = ET.ElementTree(ET.fromstring(raw_text))
|
|
||||||
root = doc.getroot()
|
|
||||||
md_ns = 'http://v8.1c.ru/8.3/MDClasses'
|
|
||||||
|
|
||||||
# Find ChildObjects
|
|
||||||
child_objects = None
|
|
||||||
if parent:
|
|
||||||
for sub in root.iter(f'{{{md_ns}}}Subsystem'):
|
|
||||||
child_objects = sub.find(f'{{{md_ns}}}ChildObjects')
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
for cfg in root.iter(f'{{{md_ns}}}Configuration'):
|
|
||||||
child_objects = cfg.find(f'{{{md_ns}}}ChildObjects')
|
|
||||||
break
|
|
||||||
|
|
||||||
if child_objects is not None:
|
|
||||||
# Check if already registered
|
|
||||||
already_exists = False
|
|
||||||
for child in child_objects:
|
|
||||||
if child.tag == f'{{{md_ns}}}Subsystem' and child.text == obj_name:
|
|
||||||
already_exists = True
|
|
||||||
break
|
|
||||||
|
|
||||||
if not already_exists:
|
|
||||||
new_el = ET.SubElement(child_objects, f'{{{md_ns}}}Subsystem')
|
|
||||||
new_el.text = obj_name
|
|
||||||
|
|
||||||
# Re-serialize with whitespace preservation via raw text manipulation instead
|
|
||||||
# Since ElementTree doesn't preserve whitespace well, use regex-based insertion
|
|
||||||
# Find </ChildObjects> or <ChildObjects/> and inject
|
|
||||||
pass # Fall through to raw text approach below
|
|
||||||
|
|
||||||
if not already_exists:
|
|
||||||
# Use raw text manipulation to preserve formatting
|
|
||||||
if '<ChildObjects/>' in raw_text:
|
|
||||||
replacement = f'<ChildObjects>\n\t\t\t<Subsystem>{esc_xml(obj_name)}</Subsystem>\n\t\t</ChildObjects>'
|
|
||||||
raw_text = raw_text.replace('<ChildObjects/>', replacement, 1)
|
|
||||||
elif '</ChildObjects>' in raw_text:
|
|
||||||
insert_line = f'\t\t\t<Subsystem>{esc_xml(obj_name)}</Subsystem>\n'
|
|
||||||
raw_text = raw_text.replace('</ChildObjects>', insert_line + '\t\t</ChildObjects>', 1)
|
|
||||||
|
|
||||||
write_utf8_bom(parent_xml_path, raw_text)
|
|
||||||
print(f"[OK] Registered in: {parent_xml_path}")
|
|
||||||
else:
|
|
||||||
print(f"[SKIP] Already registered in: {parent_xml_path}")
|
|
||||||
else:
|
|
||||||
print(f"[WARN] ChildObjects not found in: {parent_xml_path}")
|
|
||||||
else:
|
|
||||||
print("[INFO] No parent XML to register in")
|
|
||||||
|
|
||||||
# --- 6. Auto-validate ---
|
|
||||||
if not args.NoValidate:
|
|
||||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
validate_script = os.path.normpath(os.path.join(script_dir, '..', '..', 'subsystem-validate', 'scripts', 'subsystem-validate.ps1'))
|
|
||||||
if os.path.exists(validate_script):
|
|
||||||
print()
|
|
||||||
print("--- Running subsystem-validate ---")
|
|
||||||
os.system(f'powershell.exe -NoProfile -File "{validate_script}" -SubsystemPath "{target_xml}"')
|
|
||||||
|
|
||||||
# --- 7. Summary ---
|
|
||||||
print()
|
|
||||||
print("=== subsystem-compile summary ===")
|
|
||||||
print(f" Name: {obj_name}")
|
|
||||||
print(f" UUID: {uid}")
|
|
||||||
print(f" Content: {len(content_items)} objects")
|
|
||||||
print(f" Children: {len(children)}")
|
|
||||||
print(f" File: {target_xml}")
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -1,414 +0,0 @@
|
|||||||
# subsystem-edit v1.0 — Edit existing 1C subsystem XML
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)][string]$SubsystemPath,
|
|
||||||
[string]$DefinitionFile,
|
|
||||||
[ValidateSet("add-content","remove-content","add-child","remove-child","set-property")]
|
|
||||||
[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($SubsystemPath)) {
|
|
||||||
$SubsystemPath = Join-Path (Get-Location).Path $SubsystemPath
|
|
||||||
}
|
|
||||||
if (Test-Path $SubsystemPath -PathType Container) {
|
|
||||||
$dirName = Split-Path $SubsystemPath -Leaf
|
|
||||||
$candidate = Join-Path $SubsystemPath "$dirName.xml"
|
|
||||||
$sibling = Join-Path (Split-Path $SubsystemPath) "$dirName.xml"
|
|
||||||
if (Test-Path $candidate) { $SubsystemPath = $candidate }
|
|
||||||
elseif (Test-Path $sibling) { $SubsystemPath = $sibling }
|
|
||||||
else { Write-Error "No $dirName.xml found in directory or as sibling"; exit 1 }
|
|
||||||
}
|
|
||||||
# File not found — check Dir/Name/Name.xml → Dir/Name.xml
|
|
||||||
if (-not (Test-Path $SubsystemPath)) {
|
|
||||||
$fn = [System.IO.Path]::GetFileNameWithoutExtension($SubsystemPath)
|
|
||||||
$pd = Split-Path $SubsystemPath
|
|
||||||
if ($fn -eq (Split-Path $pd -Leaf)) {
|
|
||||||
$c = Join-Path (Split-Path $pd) "$fn.xml"
|
|
||||||
if (Test-Path $c) { $SubsystemPath = $c }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (-not (Test-Path $SubsystemPath)) { Write-Error "File not found: $SubsystemPath"; exit 1 }
|
|
||||||
$resolvedPath = (Resolve-Path $SubsystemPath).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:sub = $null
|
|
||||||
foreach ($child in $root.ChildNodes) {
|
|
||||||
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Subsystem") {
|
|
||||||
$script:sub = $child; break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (-not $script:sub) { Write-Error "No <Subsystem> element found"; exit 1 }
|
|
||||||
|
|
||||||
$script:propsEl = $null
|
|
||||||
$script:childObjsEl = $null
|
|
||||||
foreach ($child in $script:sub.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 "Subsystem: $($script:objName)"
|
|
||||||
|
|
||||||
# --- XML manipulation helpers (from meta-edit pattern) ---
|
|
||||||
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</_W>"
|
|
||||||
$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
|
|
||||||
}
|
|
||||||
|
|
||||||
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 the element is self-closing (empty), add whitespace for children
|
|
||||||
if (-not $container.HasChildNodes -or $container.IsEmpty) {
|
|
||||||
$childIndent = "$parentIndent`t"
|
|
||||||
# The element is self-closing; we need to add something to make it non-empty
|
|
||||||
# Adding a whitespace node will force opening+closing tags
|
|
||||||
$closeWs = $script:xmlDoc.CreateWhitespace("`r`n$parentIndent")
|
|
||||||
$container.AppendChild($closeWs) | Out-Null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Parse value: string or JSON array ---
|
|
||||||
function Parse-ValueList([string]$val) {
|
|
||||||
$val = $val.Trim()
|
|
||||||
if ($val.StartsWith("[")) {
|
|
||||||
$arr = $val | ConvertFrom-Json
|
|
||||||
$result = @(); foreach ($item in $arr) { $result += "$item" }
|
|
||||||
return ,$result
|
|
||||||
}
|
|
||||||
return @($val)
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Operations ---
|
|
||||||
function Do-AddContent([string[]]$items) {
|
|
||||||
$contentEl = $null
|
|
||||||
foreach ($child in $script:propsEl.ChildNodes) {
|
|
||||||
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Content") {
|
|
||||||
$contentEl = $child; break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (-not $contentEl) { Write-Error "No <Content> element found"; exit 1 }
|
|
||||||
|
|
||||||
# Get existing items for dedup
|
|
||||||
$existing = @()
|
|
||||||
foreach ($child in $contentEl.ChildNodes) {
|
|
||||||
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Item") {
|
|
||||||
$existing += $child.InnerText.Trim()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Determine indentation
|
|
||||||
$propsIndent = Get-ChildIndent $script:propsEl
|
|
||||||
$contentIndent = "$propsIndent`t"
|
|
||||||
|
|
||||||
# Expand self-closing if needed
|
|
||||||
if (-not $contentEl.HasChildNodes -or $contentEl.IsEmpty) {
|
|
||||||
Expand-SelfClosingElement $contentEl $propsIndent
|
|
||||||
$contentIndent = "$propsIndent`t"
|
|
||||||
} else {
|
|
||||||
$contentIndent = Get-ChildIndent $contentEl
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($item in $items) {
|
|
||||||
if ($item -in $existing) {
|
|
||||||
Warn "Content already contains: $item"
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
$fragXml = "<xr:Item xsi:type=`"xr:MDObjectRef`">$item</xr:Item>"
|
|
||||||
$nodes = Import-Fragment $fragXml
|
|
||||||
if ($nodes.Count -gt 0) {
|
|
||||||
Insert-BeforeElement $contentEl $nodes[0] $null $contentIndent
|
|
||||||
$script:addCount++
|
|
||||||
Info "Added content: $item"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Do-RemoveContent([string[]]$items) {
|
|
||||||
$contentEl = $null
|
|
||||||
foreach ($child in $script:propsEl.ChildNodes) {
|
|
||||||
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Content") {
|
|
||||||
$contentEl = $child; break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (-not $contentEl) { Write-Error "No <Content> element found"; exit 1 }
|
|
||||||
|
|
||||||
foreach ($item in $items) {
|
|
||||||
$found = $false
|
|
||||||
foreach ($child in @($contentEl.ChildNodes)) {
|
|
||||||
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Item" -and $child.InnerText.Trim() -eq $item) {
|
|
||||||
Remove-NodeWithWhitespace $child
|
|
||||||
$script:removeCount++
|
|
||||||
Info "Removed content: $item"
|
|
||||||
$found = $true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (-not $found) { Warn "Content item not found: $item" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Do-AddChild([string]$childName) {
|
|
||||||
if (-not $script:childObjsEl) { Write-Error "No <ChildObjects> element found"; exit 1 }
|
|
||||||
|
|
||||||
# Dedup check
|
|
||||||
foreach ($child in $script:childObjsEl.ChildNodes) {
|
|
||||||
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Subsystem" -and $child.InnerText.Trim() -eq $childName) {
|
|
||||||
Warn "ChildObjects already contains: $childName"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$subIndent = Get-ChildIndent $script:sub
|
|
||||||
if (-not $script:childObjsEl.HasChildNodes -or $script:childObjsEl.IsEmpty) {
|
|
||||||
Expand-SelfClosingElement $script:childObjsEl $subIndent
|
|
||||||
}
|
|
||||||
$childIndent = Get-ChildIndent $script:childObjsEl
|
|
||||||
|
|
||||||
$newEl = $script:xmlDoc.CreateElement("Subsystem", $script:mdNs)
|
|
||||||
$newEl.InnerText = $childName
|
|
||||||
Insert-BeforeElement $script:childObjsEl $newEl $null $childIndent
|
|
||||||
$script:addCount++
|
|
||||||
Info "Added child subsystem: $childName"
|
|
||||||
}
|
|
||||||
|
|
||||||
function Do-RemoveChild([string]$childName) {
|
|
||||||
if (-not $script:childObjsEl) { Write-Error "No <ChildObjects> element found"; exit 1 }
|
|
||||||
|
|
||||||
$found = $false
|
|
||||||
foreach ($child in @($script:childObjsEl.ChildNodes)) {
|
|
||||||
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Subsystem" -and $child.InnerText.Trim() -eq $childName) {
|
|
||||||
Remove-NodeWithWhitespace $child
|
|
||||||
$script:removeCount++
|
|
||||||
Info "Removed child subsystem: $childName"
|
|
||||||
$found = $true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (-not $found) { Warn "Child subsystem not found: $childName" }
|
|
||||||
}
|
|
||||||
|
|
||||||
function Do-SetProperty([string]$jsonVal) {
|
|
||||||
$propDef = $jsonVal | ConvertFrom-Json
|
|
||||||
$propName = "$($propDef.name)"
|
|
||||||
$propValue = "$($propDef.value)"
|
|
||||||
|
|
||||||
$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
|
|
||||||
}
|
|
||||||
|
|
||||||
$boolProps = @("IncludeInCommandInterface","UseOneCommand","IncludeHelpInContents")
|
|
||||||
if ($propName -in $boolProps) {
|
|
||||||
$propEl.InnerText = $propValue.ToLower()
|
|
||||||
$script:modifyCount++
|
|
||||||
Info "Set $propName = $propValue"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
$mlProps = @("Synonym","Explanation")
|
|
||||||
if ($propName -in $mlProps) {
|
|
||||||
if (-not $propValue) {
|
|
||||||
# Clear - make self-closing
|
|
||||||
$propEl.InnerXml = ""
|
|
||||||
$script:modifyCount++
|
|
||||||
Info "Cleared $propName"
|
|
||||||
} else {
|
|
||||||
$indent = Get-ChildIndent $script:propsEl
|
|
||||||
$mlXml = "`r`n$indent`t<v8:item>`r`n$indent`t`t<v8:lang>ru</v8:lang>`r`n$indent`t`t<v8:content>$([System.Security.SecurityElement]::Escape($propValue))</v8:content>`r`n$indent`t</v8:item>`r`n$indent"
|
|
||||||
$propEl.InnerXml = $mlXml
|
|
||||||
$script:modifyCount++
|
|
||||||
Info "Set $propName = `"$propValue`""
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($propName -eq "Comment") {
|
|
||||||
if (-not $propValue) { $propEl.InnerXml = "" }
|
|
||||||
else { $propEl.InnerText = $propValue }
|
|
||||||
$script:modifyCount++
|
|
||||||
Info "Set Comment = `"$propValue`""
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($propName -eq "Picture") {
|
|
||||||
if (-not $propValue) {
|
|
||||||
$propEl.InnerXml = ""
|
|
||||||
} else {
|
|
||||||
$indent = Get-ChildIndent $script:propsEl
|
|
||||||
$picXml = "`r`n$indent`t<xr:Ref>$propValue</xr:Ref>`r`n$indent`t<xr:LoadTransparent>false</xr:LoadTransparent>`r`n$indent"
|
|
||||||
$propEl.InnerXml = $picXml
|
|
||||||
}
|
|
||||||
$script:modifyCount++
|
|
||||||
Info "Set Picture = `"$propValue`""
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
# Generic text property
|
|
||||||
$propEl.InnerText = $propValue
|
|
||||||
$script:modifyCount++
|
|
||||||
Info "Set $propName = `"$propValue`""
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 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) {
|
|
||||||
"add-content" { Do-AddContent (Parse-ValueList $opValue) }
|
|
||||||
"remove-content" { Do-RemoveContent (Parse-ValueList $opValue) }
|
|
||||||
"add-child" { Do-AddChild $opValue }
|
|
||||||
"remove-child" { Do-RemoveChild $opValue }
|
|
||||||
"set-property" { Do-SetProperty $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 "..\..\subsystem-validate") "scripts\subsystem-validate.ps1"
|
|
||||||
$validateScript = [System.IO.Path]::GetFullPath($validateScript)
|
|
||||||
if (Test-Path $validateScript) {
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "--- Running subsystem-validate ---"
|
|
||||||
& powershell.exe -NoProfile -File $validateScript -SubsystemPath $resolvedPath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Summary ---
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "=== subsystem-edit summary ==="
|
|
||||||
Write-Host " Subsystem: $($script:objName)"
|
|
||||||
Write-Host " Added: $($script:addCount)"
|
|
||||||
Write-Host " Removed: $($script:removeCount)"
|
|
||||||
Write-Host " Modified: $($script:modifyCount)"
|
|
||||||
exit 0
|
|
||||||
@@ -1,464 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# subsystem-edit v1.0 — Edit existing 1C subsystem XML
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
from lxml import etree
|
|
||||||
|
|
||||||
MD_NS = "http://v8.1c.ru/8.3/MDClasses"
|
|
||||||
XR_NS = "http://v8.1c.ru/8.3/xcf/readable"
|
|
||||||
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
V8_NS = "http://v8.1c.ru/8.1/data/core"
|
|
||||||
XS_NS = "http://www.w3.org/2001/XMLSchema"
|
|
||||||
|
|
||||||
NSMAP_WRAPPER = {
|
|
||||||
None: MD_NS,
|
|
||||||
"xsi": XSI_NS,
|
|
||||||
"v8": V8_NS,
|
|
||||||
"xr": XR_NS,
|
|
||||||
"xs": XS_NS,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def localname(el):
|
|
||||||
return etree.QName(el.tag).localname
|
|
||||||
|
|
||||||
|
|
||||||
def info(msg):
|
|
||||||
print(f"[INFO] {msg}")
|
|
||||||
|
|
||||||
|
|
||||||
def warn(msg):
|
|
||||||
print(f"[WARN] {msg}")
|
|
||||||
|
|
||||||
|
|
||||||
def get_child_indent(container):
|
|
||||||
"""Detect indentation of children inside a container element."""
|
|
||||||
if container.text and "\n" in container.text:
|
|
||||||
after_nl = container.text.rsplit("\n", 1)[-1]
|
|
||||||
if after_nl and not after_nl.strip():
|
|
||||||
return after_nl
|
|
||||||
for child in container:
|
|
||||||
if child.tail and "\n" in child.tail:
|
|
||||||
after_nl = child.tail.rsplit("\n", 1)[-1]
|
|
||||||
if after_nl and not after_nl.strip():
|
|
||||||
return after_nl
|
|
||||||
# Fallback: count depth
|
|
||||||
depth = 0
|
|
||||||
current = container
|
|
||||||
while current is not None:
|
|
||||||
depth += 1
|
|
||||||
current = current.getparent()
|
|
||||||
return "\t" * depth
|
|
||||||
|
|
||||||
|
|
||||||
def insert_before_closing(container, new_el, child_indent):
|
|
||||||
"""Insert new_el before the closing tag of container, with proper indentation."""
|
|
||||||
children = list(container)
|
|
||||||
if len(children) == 0:
|
|
||||||
# Empty element: set text to newline+indent, tail of new_el to newline+parent_indent
|
|
||||||
parent_indent = child_indent[:-1] if len(child_indent) > 0 else ""
|
|
||||||
container.text = "\r\n" + child_indent
|
|
||||||
new_el.tail = "\r\n" + parent_indent
|
|
||||||
container.append(new_el)
|
|
||||||
else:
|
|
||||||
last = children[-1]
|
|
||||||
new_el.tail = last.tail
|
|
||||||
last.tail = "\r\n" + child_indent
|
|
||||||
container.append(new_el)
|
|
||||||
|
|
||||||
|
|
||||||
def remove_with_indent(el):
|
|
||||||
"""Remove element and clean up surrounding whitespace."""
|
|
||||||
parent = el.getparent()
|
|
||||||
prev = el.getprevious()
|
|
||||||
if prev is not None:
|
|
||||||
# Transfer el.tail to prev.tail
|
|
||||||
if el.tail and el.tail.strip() == "":
|
|
||||||
pass # just drop extra whitespace
|
|
||||||
prev.tail = el.tail if el.tail and el.tail.strip() else (prev.tail or "")
|
|
||||||
# Actually try to keep the prev's tail as the closing indent
|
|
||||||
# Better approach: set prev.tail to what el.tail was (newline+indent of next or closing)
|
|
||||||
if el.tail:
|
|
||||||
prev.tail = el.tail
|
|
||||||
else:
|
|
||||||
# First child: adjust parent.text
|
|
||||||
if el.tail:
|
|
||||||
parent.text = el.tail
|
|
||||||
parent.remove(el)
|
|
||||||
|
|
||||||
|
|
||||||
def expand_self_closing(container, parent_indent):
|
|
||||||
"""If container is self-closing (no children, no text), add closing whitespace."""
|
|
||||||
if len(container) == 0 and not (container.text and container.text.strip()):
|
|
||||||
container.text = "\r\n" + parent_indent
|
|
||||||
|
|
||||||
|
|
||||||
def import_fragment(xml_string, doc_root):
|
|
||||||
"""Parse an XML fragment in the MD namespace context and return elements."""
|
|
||||||
wrapper = (
|
|
||||||
f'<_W xmlns="{MD_NS}" xmlns:xsi="{XSI_NS}" xmlns:v8="{V8_NS}" '
|
|
||||||
f'xmlns:xr="{XR_NS}" xmlns:xs="{XS_NS}">{xml_string}</_W>'
|
|
||||||
)
|
|
||||||
frag = etree.fromstring(wrapper.encode("utf-8"))
|
|
||||||
nodes = []
|
|
||||||
for child in frag:
|
|
||||||
nodes.append(child)
|
|
||||||
return nodes
|
|
||||||
|
|
||||||
|
|
||||||
def parse_value_list(val):
|
|
||||||
"""Parse a string or JSON array into a list of strings."""
|
|
||||||
val = val.strip()
|
|
||||||
if val.startswith("["):
|
|
||||||
arr = json.loads(val)
|
|
||||||
return [str(item) for item in arr]
|
|
||||||
return [val]
|
|
||||||
|
|
||||||
|
|
||||||
def save_xml_bom(tree, path):
|
|
||||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
|
||||||
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"')
|
|
||||||
with open(path, "wb") as f:
|
|
||||||
f.write(b"\xef\xbb\xbf")
|
|
||||||
f.write(xml_bytes)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(description="Edit existing 1C subsystem XML", allow_abbrev=False)
|
|
||||||
parser.add_argument("-SubsystemPath", required=True)
|
|
||||||
parser.add_argument("-DefinitionFile", default=None)
|
|
||||||
parser.add_argument("-Operation", default=None, choices=["add-content", "remove-content", "add-child", "remove-child", "set-property"])
|
|
||||||
parser.add_argument("-Value", default=None)
|
|
||||||
parser.add_argument("-NoValidate", action="store_true")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
# --- Mode validation ---
|
|
||||||
if args.DefinitionFile and args.Operation:
|
|
||||||
print("Cannot use both -DefinitionFile and -Operation", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
if not args.DefinitionFile and not args.Operation:
|
|
||||||
print("Either -DefinitionFile or -Operation is required", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Resolve path ---
|
|
||||||
subsystem_path = args.SubsystemPath
|
|
||||||
if not os.path.isabs(subsystem_path):
|
|
||||||
subsystem_path = os.path.join(os.getcwd(), subsystem_path)
|
|
||||||
|
|
||||||
if os.path.isdir(subsystem_path):
|
|
||||||
dir_name = os.path.basename(subsystem_path)
|
|
||||||
candidate = os.path.join(subsystem_path, f"{dir_name}.xml")
|
|
||||||
sibling = os.path.join(os.path.dirname(subsystem_path), f"{dir_name}.xml")
|
|
||||||
if os.path.isfile(candidate):
|
|
||||||
subsystem_path = candidate
|
|
||||||
elif os.path.isfile(sibling):
|
|
||||||
subsystem_path = sibling
|
|
||||||
else:
|
|
||||||
print(f"No {dir_name}.xml found in directory or as sibling", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if not os.path.isfile(subsystem_path):
|
|
||||||
fn = os.path.splitext(os.path.basename(subsystem_path))[0]
|
|
||||||
pd = os.path.dirname(subsystem_path)
|
|
||||||
if fn == os.path.basename(pd):
|
|
||||||
c = os.path.join(os.path.dirname(pd), f"{fn}.xml")
|
|
||||||
if os.path.isfile(c):
|
|
||||||
subsystem_path = c
|
|
||||||
|
|
||||||
if not os.path.isfile(subsystem_path):
|
|
||||||
print(f"File not found: {subsystem_path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
resolved_path = os.path.abspath(subsystem_path)
|
|
||||||
|
|
||||||
# --- Load XML ---
|
|
||||||
xml_parser = etree.XMLParser(remove_blank_text=False)
|
|
||||||
tree = etree.parse(resolved_path, xml_parser)
|
|
||||||
xml_root = tree.getroot()
|
|
||||||
|
|
||||||
add_count = 0
|
|
||||||
remove_count = 0
|
|
||||||
modify_count = 0
|
|
||||||
|
|
||||||
# --- Detect structure ---
|
|
||||||
sub = None
|
|
||||||
for child in xml_root:
|
|
||||||
if isinstance(child.tag, str) and localname(child) == "Subsystem":
|
|
||||||
sub = child
|
|
||||||
break
|
|
||||||
if sub is None:
|
|
||||||
print("No <Subsystem> element found", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
props_el = None
|
|
||||||
child_objs_el = None
|
|
||||||
for child in sub:
|
|
||||||
if not isinstance(child.tag, str):
|
|
||||||
continue
|
|
||||||
if localname(child) == "Properties":
|
|
||||||
props_el = child
|
|
||||||
if localname(child) == "ChildObjects":
|
|
||||||
child_objs_el = child
|
|
||||||
|
|
||||||
obj_name = ""
|
|
||||||
if props_el is not None:
|
|
||||||
for child in props_el:
|
|
||||||
if isinstance(child.tag, str) and localname(child) == "Name":
|
|
||||||
obj_name = (child.text or "").strip()
|
|
||||||
break
|
|
||||||
info(f"Subsystem: {obj_name}")
|
|
||||||
|
|
||||||
# --- Operations ---
|
|
||||||
def do_add_content(items):
|
|
||||||
nonlocal add_count
|
|
||||||
content_el = None
|
|
||||||
for child in props_el:
|
|
||||||
if isinstance(child.tag, str) and localname(child) == "Content":
|
|
||||||
content_el = child
|
|
||||||
break
|
|
||||||
if content_el is None:
|
|
||||||
print("No <Content> element found", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
existing = set()
|
|
||||||
for child in content_el:
|
|
||||||
if isinstance(child.tag, str) and localname(child) == "Item":
|
|
||||||
existing.add((child.text or "").strip())
|
|
||||||
|
|
||||||
props_indent = get_child_indent(props_el)
|
|
||||||
if len(content_el) == 0 and not (content_el.text and content_el.text.strip()):
|
|
||||||
expand_self_closing(content_el, props_indent)
|
|
||||||
content_indent = get_child_indent(content_el)
|
|
||||||
|
|
||||||
for item in items:
|
|
||||||
if item in existing:
|
|
||||||
warn(f"Content already contains: {item}")
|
|
||||||
continue
|
|
||||||
frag_xml = f'<xr:Item xsi:type="xr:MDObjectRef">{item}</xr:Item>'
|
|
||||||
nodes = import_fragment(frag_xml, xml_root)
|
|
||||||
if nodes:
|
|
||||||
insert_before_closing(content_el, nodes[0], content_indent)
|
|
||||||
add_count += 1
|
|
||||||
info(f"Added content: {item}")
|
|
||||||
|
|
||||||
def do_remove_content(items):
|
|
||||||
nonlocal remove_count
|
|
||||||
content_el = None
|
|
||||||
for child in props_el:
|
|
||||||
if isinstance(child.tag, str) and localname(child) == "Content":
|
|
||||||
content_el = child
|
|
||||||
break
|
|
||||||
if content_el is None:
|
|
||||||
print("No <Content> element found", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
for item in items:
|
|
||||||
found = False
|
|
||||||
for child in list(content_el):
|
|
||||||
if isinstance(child.tag, str) and localname(child) == "Item" and (child.text or "").strip() == item:
|
|
||||||
remove_with_indent(child)
|
|
||||||
remove_count += 1
|
|
||||||
info(f"Removed content: {item}")
|
|
||||||
found = True
|
|
||||||
break
|
|
||||||
if not found:
|
|
||||||
warn(f"Content item not found: {item}")
|
|
||||||
|
|
||||||
def do_add_child(child_name):
|
|
||||||
nonlocal add_count
|
|
||||||
if child_objs_el is None:
|
|
||||||
print("No <ChildObjects> element found", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
for child in child_objs_el:
|
|
||||||
if isinstance(child.tag, str) and localname(child) == "Subsystem" and (child.text or "").strip() == child_name:
|
|
||||||
warn(f"ChildObjects already contains: {child_name}")
|
|
||||||
return
|
|
||||||
|
|
||||||
sub_indent = get_child_indent(sub)
|
|
||||||
if len(child_objs_el) == 0 and not (child_objs_el.text and child_objs_el.text.strip()):
|
|
||||||
expand_self_closing(child_objs_el, sub_indent)
|
|
||||||
ci = get_child_indent(child_objs_el)
|
|
||||||
|
|
||||||
new_el = etree.SubElement(child_objs_el, f"{{{MD_NS}}}Subsystem")
|
|
||||||
# Actually we need to use insert_before_closing pattern
|
|
||||||
child_objs_el.remove(new_el)
|
|
||||||
new_el = etree.Element(f"{{{MD_NS}}}Subsystem")
|
|
||||||
new_el.text = child_name
|
|
||||||
insert_before_closing(child_objs_el, new_el, ci)
|
|
||||||
add_count += 1
|
|
||||||
info(f"Added child subsystem: {child_name}")
|
|
||||||
|
|
||||||
def do_remove_child(child_name):
|
|
||||||
nonlocal remove_count
|
|
||||||
if child_objs_el is None:
|
|
||||||
print("No <ChildObjects> element found", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
found = False
|
|
||||||
for child in list(child_objs_el):
|
|
||||||
if isinstance(child.tag, str) and localname(child) == "Subsystem" and (child.text or "").strip() == child_name:
|
|
||||||
remove_with_indent(child)
|
|
||||||
remove_count += 1
|
|
||||||
info(f"Removed child subsystem: {child_name}")
|
|
||||||
found = True
|
|
||||||
break
|
|
||||||
if not found:
|
|
||||||
warn(f"Child subsystem not found: {child_name}")
|
|
||||||
|
|
||||||
def do_set_property(json_val):
|
|
||||||
nonlocal modify_count
|
|
||||||
prop_def = json.loads(json_val)
|
|
||||||
prop_name = str(prop_def["name"])
|
|
||||||
prop_value = str(prop_def.get("value", ""))
|
|
||||||
|
|
||||||
prop_el = None
|
|
||||||
for child in props_el:
|
|
||||||
if isinstance(child.tag, str) and localname(child) == prop_name:
|
|
||||||
prop_el = child
|
|
||||||
break
|
|
||||||
if prop_el is None:
|
|
||||||
print(f"Property '{prop_name}' not found in Properties", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
bool_props = ["IncludeInCommandInterface", "UseOneCommand", "IncludeHelpInContents"]
|
|
||||||
if prop_name in bool_props:
|
|
||||||
prop_el.text = prop_value.lower()
|
|
||||||
# Clear children
|
|
||||||
for ch in list(prop_el):
|
|
||||||
prop_el.remove(ch)
|
|
||||||
modify_count += 1
|
|
||||||
info(f"Set {prop_name} = {prop_value}")
|
|
||||||
return
|
|
||||||
|
|
||||||
ml_props = ["Synonym", "Explanation"]
|
|
||||||
if prop_name in ml_props:
|
|
||||||
if not prop_value:
|
|
||||||
# Clear - make self-closing
|
|
||||||
for ch in list(prop_el):
|
|
||||||
prop_el.remove(ch)
|
|
||||||
prop_el.text = None
|
|
||||||
modify_count += 1
|
|
||||||
info(f"Cleared {prop_name}")
|
|
||||||
else:
|
|
||||||
for ch in list(prop_el):
|
|
||||||
prop_el.remove(ch)
|
|
||||||
indent = get_child_indent(props_el)
|
|
||||||
|
|
||||||
item_el = etree.SubElement(prop_el, f"{{{V8_NS}}}item")
|
|
||||||
lang_el = etree.SubElement(item_el, f"{{{V8_NS}}}lang")
|
|
||||||
lang_el.text = "ru"
|
|
||||||
content_el = etree.SubElement(item_el, f"{{{V8_NS}}}content")
|
|
||||||
content_el.text = prop_value
|
|
||||||
|
|
||||||
# Set whitespace
|
|
||||||
prop_el.text = "\r\n" + indent + "\t"
|
|
||||||
item_el.text = "\r\n" + indent + "\t\t"
|
|
||||||
lang_el.tail = "\r\n" + indent + "\t\t"
|
|
||||||
content_el.tail = "\r\n" + indent + "\t"
|
|
||||||
item_el.tail = "\r\n" + indent
|
|
||||||
|
|
||||||
modify_count += 1
|
|
||||||
info(f'Set {prop_name} = "{prop_value}"')
|
|
||||||
return
|
|
||||||
|
|
||||||
if prop_name == "Comment":
|
|
||||||
for ch in list(prop_el):
|
|
||||||
prop_el.remove(ch)
|
|
||||||
if not prop_value:
|
|
||||||
prop_el.text = None
|
|
||||||
else:
|
|
||||||
prop_el.text = prop_value
|
|
||||||
modify_count += 1
|
|
||||||
info(f'Set Comment = "{prop_value}"')
|
|
||||||
return
|
|
||||||
|
|
||||||
if prop_name == "Picture":
|
|
||||||
for ch in list(prop_el):
|
|
||||||
prop_el.remove(ch)
|
|
||||||
if not prop_value:
|
|
||||||
prop_el.text = None
|
|
||||||
else:
|
|
||||||
indent = get_child_indent(props_el)
|
|
||||||
ref_el = etree.SubElement(prop_el, f"{{{XR_NS}}}Ref")
|
|
||||||
ref_el.text = prop_value
|
|
||||||
load_el = etree.SubElement(prop_el, f"{{{XR_NS}}}LoadTransparent")
|
|
||||||
load_el.text = "false"
|
|
||||||
prop_el.text = "\r\n" + indent + "\t"
|
|
||||||
ref_el.tail = "\r\n" + indent + "\t"
|
|
||||||
load_el.tail = "\r\n" + indent
|
|
||||||
modify_count += 1
|
|
||||||
info(f'Set Picture = "{prop_value}"')
|
|
||||||
return
|
|
||||||
|
|
||||||
# Generic text property
|
|
||||||
for ch in list(prop_el):
|
|
||||||
prop_el.remove(ch)
|
|
||||||
prop_el.text = prop_value
|
|
||||||
modify_count += 1
|
|
||||||
info(f'Set {prop_name} = "{prop_value}"')
|
|
||||||
|
|
||||||
# --- Execute operations ---
|
|
||||||
operations = []
|
|
||||||
if args.DefinitionFile:
|
|
||||||
def_file = args.DefinitionFile
|
|
||||||
if not os.path.isabs(def_file):
|
|
||||||
def_file = os.path.join(os.getcwd(), def_file)
|
|
||||||
with open(def_file, "r", encoding="utf-8-sig") as fh:
|
|
||||||
ops = json.loads(fh.read())
|
|
||||||
if isinstance(ops, list):
|
|
||||||
operations = ops
|
|
||||||
else:
|
|
||||||
operations = [ops]
|
|
||||||
else:
|
|
||||||
operations = [{"operation": args.Operation, "value": args.Value or ""}]
|
|
||||||
|
|
||||||
for op in operations:
|
|
||||||
op_name = op.get("operation", args.Operation or "")
|
|
||||||
op_value = op.get("value", args.Value or "")
|
|
||||||
|
|
||||||
if op_name == "add-content":
|
|
||||||
do_add_content(parse_value_list(op_value))
|
|
||||||
elif op_name == "remove-content":
|
|
||||||
do_remove_content(parse_value_list(op_value))
|
|
||||||
elif op_name == "add-child":
|
|
||||||
do_add_child(op_value)
|
|
||||||
elif op_name == "remove-child":
|
|
||||||
do_remove_child(op_value)
|
|
||||||
elif op_name == "set-property":
|
|
||||||
do_set_property(op_value)
|
|
||||||
else:
|
|
||||||
print(f"Unknown operation: {op_name}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Save ---
|
|
||||||
save_xml_bom(tree, resolved_path)
|
|
||||||
info(f"Saved: {resolved_path}")
|
|
||||||
|
|
||||||
# --- Auto-validate ---
|
|
||||||
if not args.NoValidate:
|
|
||||||
validate_script = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "..", "subsystem-validate", "scripts", "subsystem-validate.py"))
|
|
||||||
if os.path.isfile(validate_script):
|
|
||||||
print()
|
|
||||||
print("--- Running subsystem-validate ---")
|
|
||||||
subprocess.run([sys.executable, validate_script, "-SubsystemPath", resolved_path])
|
|
||||||
|
|
||||||
# --- Summary ---
|
|
||||||
print()
|
|
||||||
print("=== subsystem-edit summary ===")
|
|
||||||
print(f" Subsystem: {obj_name}")
|
|
||||||
print(f" Added: {add_count}")
|
|
||||||
print(f" Removed: {remove_count}")
|
|
||||||
print(f" Modified: {modify_count}")
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
---
|
|
||||||
name: subsystem-validate
|
|
||||||
description: Валидация подсистемы 1С. Используй после создания или модификации подсистемы для проверки корректности
|
|
||||||
argument-hint: <SubsystemPath> [-Detailed] [-MaxErrors 30]
|
|
||||||
allowed-tools:
|
|
||||||
- Bash
|
|
||||||
- Read
|
|
||||||
- Glob
|
|
||||||
---
|
|
||||||
|
|
||||||
# /subsystem-validate — валидация подсистемы 1С
|
|
||||||
|
|
||||||
Проверяет структурную корректность XML-файла подсистемы из выгрузки конфигурации.
|
|
||||||
|
|
||||||
## Параметры
|
|
||||||
|
|
||||||
| Параметр | Обяз. | Умолч. | Описание |
|
|
||||||
|---------------|:-----:|---------|--------------------------------------------|
|
|
||||||
| SubsystemPath | да | — | Путь к XML-файлу подсистемы |
|
|
||||||
| Detailed | нет | — | Показывать [OK] для каждой проверки |
|
|
||||||
| MaxErrors | нет | 30 | Остановиться после N ошибок |
|
|
||||||
| OutFile | нет | — | Записать результат в файл |
|
|
||||||
|
|
||||||
## Команда
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
powershell.exe -NoProfile -File ".claude/skills/subsystem-validate/scripts/subsystem-validate.ps1" -SubsystemPath "Subsystems/Продажи"
|
|
||||||
powershell.exe -NoProfile -File ".claude/skills/subsystem-validate/scripts/subsystem-validate.ps1" -SubsystemPath "Subsystems/Продажи.xml"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Проверки (13)
|
|
||||||
|
|
||||||
| # | Проверка | Серьёзность |
|
|
||||||
|---|----------|-------------|
|
|
||||||
| 1 | XML well-formedness + root structure (MetaDataObject/Subsystem) | ERROR |
|
|
||||||
| 2 | Properties — 9 обязательных свойств | ERROR |
|
|
||||||
| 3 | Name — непустой, валидный идентификатор | ERROR |
|
|
||||||
| 4 | Synonym — непустой (хотя бы один v8:item) | WARN |
|
|
||||||
| 5 | Булевы свойства — содержат true/false | ERROR |
|
|
||||||
| 6 | Content — формат xr:Item, xsi:type | ERROR |
|
|
||||||
| 7 | Content — нет дубликатов | WARN |
|
|
||||||
| 8 | ChildObjects — элементы непустые | ERROR |
|
|
||||||
| 9 | ChildObjects — нет дубликатов | WARN |
|
|
||||||
| 10 | ChildObjects → файлы существуют | WARN |
|
|
||||||
| 11 | CommandInterface.xml — well-formedness | ERROR |
|
|
||||||
| 12 | Picture — формат ссылки | ERROR |
|
|
||||||
| 13 | UseOneCommand=true → ровно 1 элемент в Content | ERROR |
|
|
||||||
|
|
||||||
Exit code: 0 = OK, 1 = есть ошибки. По умолчанию краткий вывод. `-Detailed` для поштучной детализации.
|
|
||||||
@@ -1,224 +0,0 @@
|
|||||||
# template-add v1.1 — Add template to 1C object
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[Alias("ProcessorName")]
|
|
||||||
[string]$ObjectName,
|
|
||||||
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[string]$TemplateName,
|
|
||||||
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[ValidateSet("HTML", "Text", "SpreadsheetDocument", "BinaryData", "DataCompositionSchema")]
|
|
||||||
[string]$TemplateType,
|
|
||||||
|
|
||||||
[string]$Synonym = $TemplateName,
|
|
||||||
|
|
||||||
[string]$SrcDir = "src",
|
|
||||||
|
|
||||||
[switch]$SetMainSKD
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
|
|
||||||
# --- Маппинг типов ---
|
|
||||||
|
|
||||||
$typeMap = @{
|
|
||||||
"HTML" = @{ TemplateType = "HTMLDocument"; Ext = ".html" }
|
|
||||||
"Text" = @{ TemplateType = "TextDocument"; Ext = ".txt" }
|
|
||||||
"SpreadsheetDocument" = @{ TemplateType = "SpreadsheetDocument"; Ext = ".xml" }
|
|
||||||
"BinaryData" = @{ TemplateType = "BinaryData"; Ext = ".bin" }
|
|
||||||
"DataCompositionSchema" = @{ TemplateType = "DataCompositionSchema"; Ext = ".xml" }
|
|
||||||
}
|
|
||||||
|
|
||||||
$tmpl = $typeMap[$TemplateType]
|
|
||||||
|
|
||||||
# --- Проверки ---
|
|
||||||
|
|
||||||
$rootXmlPath = Join-Path $SrcDir "$ObjectName.xml"
|
|
||||||
if (-not (Test-Path $rootXmlPath)) {
|
|
||||||
Write-Error "Корневой файл обработки не найден: $rootXmlPath"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
$processorDir = Join-Path $SrcDir $ObjectName
|
|
||||||
$templatesDir = Join-Path $processorDir "Templates"
|
|
||||||
$templateMetaPath = Join-Path $templatesDir "$TemplateName.xml"
|
|
||||||
|
|
||||||
if (Test-Path $templateMetaPath) {
|
|
||||||
Write-Error "Макет уже существует: $templateMetaPath"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Создание каталогов ---
|
|
||||||
|
|
||||||
$templateExtDir = Join-Path (Join-Path $templatesDir $TemplateName) "Ext"
|
|
||||||
New-Item -ItemType Directory -Path $templateExtDir -Force | Out-Null
|
|
||||||
|
|
||||||
# --- Кодировка ---
|
|
||||||
|
|
||||||
$encBom = New-Object System.Text.UTF8Encoding($true)
|
|
||||||
|
|
||||||
# --- 1. Метаданные макета (Templates/<TemplateName>.xml) ---
|
|
||||||
|
|
||||||
$templateUuid = [guid]::NewGuid().ToString()
|
|
||||||
|
|
||||||
$templateMetaXml = @"
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<MetaDataObject 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" version="2.17">
|
|
||||||
<Template uuid="$templateUuid">
|
|
||||||
<Properties>
|
|
||||||
<Name>$TemplateName</Name>
|
|
||||||
<Synonym>
|
|
||||||
<v8:item>
|
|
||||||
<v8:lang>ru</v8:lang>
|
|
||||||
<v8:content>$Synonym</v8:content>
|
|
||||||
</v8:item>
|
|
||||||
</Synonym>
|
|
||||||
<Comment/>
|
|
||||||
<TemplateType>$($tmpl.TemplateType)</TemplateType>
|
|
||||||
</Properties>
|
|
||||||
</Template>
|
|
||||||
</MetaDataObject>
|
|
||||||
"@
|
|
||||||
|
|
||||||
[System.IO.File]::WriteAllText($templateMetaPath, $templateMetaXml, $encBom)
|
|
||||||
|
|
||||||
# --- 2. Содержимое макета (Templates/<TemplateName>/Ext/Template.<ext>) ---
|
|
||||||
|
|
||||||
$templateFilePath = Join-Path $templateExtDir "Template$($tmpl.Ext)"
|
|
||||||
|
|
||||||
switch ($TemplateType) {
|
|
||||||
"HTML" {
|
|
||||||
$content = @"
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title></title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
"@
|
|
||||||
[System.IO.File]::WriteAllText($templateFilePath, $content, $encBom)
|
|
||||||
}
|
|
||||||
"Text" {
|
|
||||||
[System.IO.File]::WriteAllText($templateFilePath, "", $encBom)
|
|
||||||
}
|
|
||||||
"SpreadsheetDocument" {
|
|
||||||
$content = @"
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<SpreadsheetDocument xmlns="http://v8.1c.ru/spreadsheet/document" xmlns:ss="http://v8.1c.ru/spreadsheet/document" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
|
||||||
</SpreadsheetDocument>
|
|
||||||
"@
|
|
||||||
[System.IO.File]::WriteAllText($templateFilePath, $content, $encBom)
|
|
||||||
}
|
|
||||||
"BinaryData" {
|
|
||||||
[System.IO.File]::WriteAllBytes($templateFilePath, @())
|
|
||||||
}
|
|
||||||
"DataCompositionSchema" {
|
|
||||||
$content = @"
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
|
|
||||||
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
|
|
||||||
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
|
|
||||||
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
|
|
||||||
xmlns:v8="http://v8.1c.ru/8.1/data/core"
|
|
||||||
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
|
|
||||||
xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
|
||||||
<dataSource>
|
|
||||||
<name>ИсточникДанных1</name>
|
|
||||||
<dataSourceType>Local</dataSourceType>
|
|
||||||
</dataSource>
|
|
||||||
</DataCompositionSchema>
|
|
||||||
"@
|
|
||||||
[System.IO.File]::WriteAllText($templateFilePath, $content, $encBom)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 3. Модификация корневого XML ---
|
|
||||||
|
|
||||||
$rootXmlFull = Resolve-Path $rootXmlPath
|
|
||||||
$xmlDoc = New-Object System.Xml.XmlDocument
|
|
||||||
$xmlDoc.PreserveWhitespace = $true
|
|
||||||
$xmlDoc.Load($rootXmlFull.Path)
|
|
||||||
|
|
||||||
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
|
|
||||||
$nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
|
||||||
|
|
||||||
$childObjects = $xmlDoc.SelectSingleNode("//md:ChildObjects", $nsMgr)
|
|
||||||
if (-not $childObjects) {
|
|
||||||
Write-Error "Не найден элемент ChildObjects в $rootXmlPath"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Добавить <Template> в конец ChildObjects
|
|
||||||
$templateElem = $xmlDoc.CreateElement("Template", "http://v8.1c.ru/8.3/MDClasses")
|
|
||||||
$templateElem.InnerText = $TemplateName
|
|
||||||
|
|
||||||
if ($childObjects.ChildNodes.Count -eq 0) {
|
|
||||||
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t`t")) | Out-Null
|
|
||||||
$childObjects.AppendChild($templateElem) | Out-Null
|
|
||||||
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t")) | Out-Null
|
|
||||||
} else {
|
|
||||||
$lastChild = $childObjects.LastChild
|
|
||||||
# Вставить перед закрывающим whitespace (если есть), или в конец
|
|
||||||
if ($lastChild.NodeType -eq [System.Xml.XmlNodeType]::Whitespace) {
|
|
||||||
$childObjects.InsertBefore($xmlDoc.CreateWhitespace("`n`t`t`t"), $lastChild) | Out-Null
|
|
||||||
$childObjects.InsertBefore($templateElem, $lastChild) | Out-Null
|
|
||||||
} else {
|
|
||||||
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t`t")) | Out-Null
|
|
||||||
$childObjects.AppendChild($templateElem) | Out-Null
|
|
||||||
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t")) | Out-Null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 4. MainDataCompositionSchema (для ExternalReport / Report) ---
|
|
||||||
|
|
||||||
$mainDCSUpdated = $false
|
|
||||||
if ($TemplateType -eq "DataCompositionSchema") {
|
|
||||||
# Определяем корневой элемент объекта
|
|
||||||
$reportLikeTypes = @("ExternalReport", "Report")
|
|
||||||
$objectTypeNode = $null
|
|
||||||
$objectTypeName = $null
|
|
||||||
foreach ($rt in $reportLikeTypes) {
|
|
||||||
$node = $xmlDoc.SelectSingleNode("//md:$rt", $nsMgr)
|
|
||||||
if ($node) {
|
|
||||||
$objectTypeNode = $node
|
|
||||||
$objectTypeName = $rt
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($objectTypeNode) {
|
|
||||||
$mainDCS = $xmlDoc.SelectSingleNode("//md:${objectTypeName}/md:Properties/md:MainDataCompositionSchema", $nsMgr)
|
|
||||||
if ($mainDCS) {
|
|
||||||
$isEmpty = [string]::IsNullOrWhiteSpace($mainDCS.InnerText)
|
|
||||||
if ($isEmpty -or $SetMainSKD) {
|
|
||||||
$objName = $xmlDoc.SelectSingleNode("//md:${objectTypeName}/md:Properties/md:Name", $nsMgr).InnerText
|
|
||||||
$mainDCS.InnerText = "$objectTypeName.$objName.Template.$TemplateName"
|
|
||||||
$mainDCSUpdated = $true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Сохранить с BOM
|
|
||||||
$settings = New-Object System.Xml.XmlWriterSettings
|
|
||||||
$settings.Encoding = $encBom
|
|
||||||
$settings.Indent = $false
|
|
||||||
|
|
||||||
$stream = New-Object System.IO.FileStream($rootXmlFull.Path, [System.IO.FileMode]::Create)
|
|
||||||
$writer = [System.Xml.XmlWriter]::Create($stream, $settings)
|
|
||||||
$xmlDoc.Save($writer)
|
|
||||||
$writer.Close()
|
|
||||||
$stream.Close()
|
|
||||||
|
|
||||||
Write-Host "[OK] Создан макет: $TemplateName ($TemplateType)"
|
|
||||||
Write-Host " Метаданные: $templateMetaPath"
|
|
||||||
Write-Host " Содержимое: $templateFilePath"
|
|
||||||
if ($mainDCSUpdated) {
|
|
||||||
Write-Host " MainDataCompositionSchema: $($mainDCS.InnerText)"
|
|
||||||
}
|
|
||||||
@@ -1,251 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# add-template v1.0 — Add template to 1C object
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
from lxml import etree
|
|
||||||
|
|
||||||
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
|
|
||||||
|
|
||||||
TYPE_MAP = {
|
|
||||||
"HTML": {"TemplateType": "HTMLDocument", "Ext": ".html"},
|
|
||||||
"Text": {"TemplateType": "TextDocument", "Ext": ".txt"},
|
|
||||||
"SpreadsheetDocument": {"TemplateType": "SpreadsheetDocument", "Ext": ".xml"},
|
|
||||||
"BinaryData": {"TemplateType": "BinaryData", "Ext": ".bin"},
|
|
||||||
"DataCompositionSchema": {"TemplateType": "DataCompositionSchema", "Ext": ".xml"},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def save_xml_with_bom(tree, path):
|
|
||||||
"""Save XML tree to file with UTF-8 BOM."""
|
|
||||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
|
||||||
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"')
|
|
||||||
with open(path, "wb") as f:
|
|
||||||
f.write(b"\xef\xbb\xbf")
|
|
||||||
f.write(xml_bytes)
|
|
||||||
|
|
||||||
|
|
||||||
def write_text_with_bom(path, text):
|
|
||||||
"""Write text to file with UTF-8 BOM."""
|
|
||||||
with open(path, "w", encoding="utf-8-sig") as f:
|
|
||||||
f.write(text)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(description="Add template to 1C object", allow_abbrev=False)
|
|
||||||
parser.add_argument("-ObjectName", "-ProcessorName", required=True)
|
|
||||||
parser.add_argument("-TemplateName", required=True)
|
|
||||||
parser.add_argument("-TemplateType", required=True,
|
|
||||||
choices=["HTML", "Text", "SpreadsheetDocument", "BinaryData", "DataCompositionSchema"])
|
|
||||||
parser.add_argument("-Synonym", default=None)
|
|
||||||
parser.add_argument("-SrcDir", default="src")
|
|
||||||
parser.add_argument("-SetMainSKD", action="store_true")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
object_name = args.ObjectName
|
|
||||||
template_name = args.TemplateName
|
|
||||||
template_type = args.TemplateType
|
|
||||||
synonym = args.Synonym if args.Synonym is not None else template_name
|
|
||||||
src_dir = args.SrcDir
|
|
||||||
set_main_skd = args.SetMainSKD
|
|
||||||
|
|
||||||
tmpl = TYPE_MAP[template_type]
|
|
||||||
|
|
||||||
# --- Checks ---
|
|
||||||
|
|
||||||
root_xml_path = os.path.join(src_dir, f"{object_name}.xml")
|
|
||||||
if not os.path.exists(root_xml_path):
|
|
||||||
print(f"Корневой файл обработки не найден: {root_xml_path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
processor_dir = os.path.join(src_dir, object_name)
|
|
||||||
templates_dir = os.path.join(processor_dir, "Templates")
|
|
||||||
template_meta_path = os.path.join(templates_dir, f"{template_name}.xml")
|
|
||||||
|
|
||||||
if os.path.exists(template_meta_path):
|
|
||||||
print(f"Макет уже существует: {template_meta_path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Create directories ---
|
|
||||||
|
|
||||||
template_ext_dir = os.path.join(templates_dir, template_name, "Ext")
|
|
||||||
os.makedirs(template_ext_dir, exist_ok=True)
|
|
||||||
|
|
||||||
# --- 1. Template metadata (Templates/<TemplateName>.xml) ---
|
|
||||||
|
|
||||||
template_uuid = str(uuid.uuid4())
|
|
||||||
|
|
||||||
template_meta_xml = (
|
|
||||||
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
|
||||||
'<MetaDataObject 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"'
|
|
||||||
' version="2.17">\n'
|
|
||||||
f'\t<Template uuid="{template_uuid}">\n'
|
|
||||||
'\t\t<Properties>\n'
|
|
||||||
f'\t\t\t<Name>{template_name}</Name>\n'
|
|
||||||
'\t\t\t<Synonym>\n'
|
|
||||||
'\t\t\t\t<v8:item>\n'
|
|
||||||
'\t\t\t\t\t<v8:lang>ru</v8:lang>\n'
|
|
||||||
f'\t\t\t\t\t<v8:content>{synonym}</v8:content>\n'
|
|
||||||
'\t\t\t\t</v8:item>\n'
|
|
||||||
'\t\t\t</Synonym>\n'
|
|
||||||
'\t\t\t<Comment/>\n'
|
|
||||||
f'\t\t\t<TemplateType>{tmpl["TemplateType"]}</TemplateType>\n'
|
|
||||||
'\t\t</Properties>\n'
|
|
||||||
'\t</Template>\n'
|
|
||||||
'</MetaDataObject>'
|
|
||||||
)
|
|
||||||
|
|
||||||
write_text_with_bom(template_meta_path, template_meta_xml)
|
|
||||||
|
|
||||||
# --- 2. Template content (Templates/<TemplateName>/Ext/Template.<ext>) ---
|
|
||||||
|
|
||||||
template_file_path = os.path.join(template_ext_dir, f"Template{tmpl['Ext']}")
|
|
||||||
|
|
||||||
if template_type == "HTML":
|
|
||||||
content = (
|
|
||||||
'<!DOCTYPE html>\n'
|
|
||||||
'<html>\n'
|
|
||||||
'<head>\n'
|
|
||||||
'\t<meta charset="UTF-8">\n'
|
|
||||||
'\t<title></title>\n'
|
|
||||||
'</head>\n'
|
|
||||||
'<body>\n'
|
|
||||||
'</body>\n'
|
|
||||||
'</html>'
|
|
||||||
)
|
|
||||||
write_text_with_bom(template_file_path, content)
|
|
||||||
|
|
||||||
elif template_type == "Text":
|
|
||||||
write_text_with_bom(template_file_path, "")
|
|
||||||
|
|
||||||
elif template_type == "SpreadsheetDocument":
|
|
||||||
content = (
|
|
||||||
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
|
||||||
'<SpreadsheetDocument xmlns="http://v8.1c.ru/spreadsheet/document"'
|
|
||||||
' xmlns:ss="http://v8.1c.ru/spreadsheet/document"'
|
|
||||||
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
|
|
||||||
' xmlns:xs="http://www.w3.org/2001/XMLSchema">\n'
|
|
||||||
'</SpreadsheetDocument>'
|
|
||||||
)
|
|
||||||
write_text_with_bom(template_file_path, content)
|
|
||||||
|
|
||||||
elif template_type == "BinaryData":
|
|
||||||
with open(template_file_path, "wb") as f:
|
|
||||||
pass # empty file
|
|
||||||
|
|
||||||
elif template_type == "DataCompositionSchema":
|
|
||||||
content = (
|
|
||||||
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
|
||||||
'<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"\n'
|
|
||||||
'\t\txmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"\n'
|
|
||||||
'\t\txmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"\n'
|
|
||||||
'\t\txmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"\n'
|
|
||||||
'\t\txmlns:v8="http://v8.1c.ru/8.1/data/core"\n'
|
|
||||||
'\t\txmlns:v8ui="http://v8.1c.ru/8.1/data/ui"\n'
|
|
||||||
'\t\txmlns:xs="http://www.w3.org/2001/XMLSchema"\n'
|
|
||||||
'\t\txmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">\n'
|
|
||||||
'\t<dataSource>\n'
|
|
||||||
'\t\t<name>ИсточникДанных1</name>\n'
|
|
||||||
'\t\t<dataSourceType>Local</dataSourceType>\n'
|
|
||||||
'\t</dataSource>\n'
|
|
||||||
'</DataCompositionSchema>'
|
|
||||||
)
|
|
||||||
write_text_with_bom(template_file_path, content)
|
|
||||||
|
|
||||||
# --- 3. Modify root XML ---
|
|
||||||
|
|
||||||
root_xml_full = os.path.abspath(root_xml_path)
|
|
||||||
parser_xml = etree.XMLParser(remove_blank_text=False)
|
|
||||||
tree = etree.parse(root_xml_full, parser_xml)
|
|
||||||
root = tree.getroot()
|
|
||||||
|
|
||||||
ns = "http://v8.1c.ru/8.3/MDClasses"
|
|
||||||
child_objects = root.find(".//md:ChildObjects", NSMAP)
|
|
||||||
if child_objects is None:
|
|
||||||
print(f"Не найден элемент ChildObjects в {root_xml_path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Add <Template> to end of ChildObjects
|
|
||||||
template_elem = etree.SubElement(child_objects, f"{{{ns}}}Template")
|
|
||||||
template_elem.text = template_name
|
|
||||||
# Remove auto-appended element to reinsert with proper whitespace
|
|
||||||
child_objects.remove(template_elem)
|
|
||||||
|
|
||||||
children = list(child_objects)
|
|
||||||
if len(children) == 0 and (child_objects.text is None or child_objects.text.strip() == ""):
|
|
||||||
# Empty ChildObjects (self-closing)
|
|
||||||
child_objects.text = "\n\t\t\t"
|
|
||||||
child_objects.append(template_elem)
|
|
||||||
template_elem.tail = "\n\t\t"
|
|
||||||
else:
|
|
||||||
if len(children) > 0:
|
|
||||||
last_child = children[-1]
|
|
||||||
# last_child.tail is the trailing whitespace before </ChildObjects>
|
|
||||||
old_tail = last_child.tail
|
|
||||||
last_child.tail = "\n\t\t\t"
|
|
||||||
child_objects.append(template_elem)
|
|
||||||
template_elem.tail = old_tail if old_tail else "\n\t\t"
|
|
||||||
else:
|
|
||||||
# Has text content but no element children
|
|
||||||
child_objects.text = (child_objects.text or "") + "\n\t\t\t"
|
|
||||||
child_objects.append(template_elem)
|
|
||||||
template_elem.tail = "\n\t\t"
|
|
||||||
|
|
||||||
# --- 4. MainDataCompositionSchema (for ExternalReport / Report) ---
|
|
||||||
|
|
||||||
main_dcs_updated = False
|
|
||||||
if template_type == "DataCompositionSchema":
|
|
||||||
report_like_types = ["ExternalReport", "Report"]
|
|
||||||
object_type_node = None
|
|
||||||
object_type_name = None
|
|
||||||
for rt in report_like_types:
|
|
||||||
node = root.find(f".//md:{rt}", NSMAP)
|
|
||||||
if node is not None:
|
|
||||||
object_type_node = node
|
|
||||||
object_type_name = rt
|
|
||||||
break
|
|
||||||
|
|
||||||
if object_type_node is not None:
|
|
||||||
main_dcs = root.find(f".//md:{object_type_name}/md:Properties/md:MainDataCompositionSchema", NSMAP)
|
|
||||||
if main_dcs is not None:
|
|
||||||
is_empty = main_dcs.text is None or main_dcs.text.strip() == ""
|
|
||||||
if is_empty or set_main_skd:
|
|
||||||
obj_name_node = root.find(f".//md:{object_type_name}/md:Properties/md:Name", NSMAP)
|
|
||||||
obj_name = obj_name_node.text if obj_name_node is not None else ""
|
|
||||||
main_dcs.text = f"{object_type_name}.{obj_name}.Template.{template_name}"
|
|
||||||
main_dcs_updated = True
|
|
||||||
|
|
||||||
# Save with BOM
|
|
||||||
save_xml_with_bom(tree, root_xml_full)
|
|
||||||
|
|
||||||
print(f"[OK] Создан макет: {template_name} ({template_type})")
|
|
||||||
print(f" Метаданные: {template_meta_path}")
|
|
||||||
print(f" Содержимое: {template_file_path}")
|
|
||||||
if main_dcs_updated:
|
|
||||||
print(f" MainDataCompositionSchema: {main_dcs.text}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,374 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
// web-test run v1.2 — CLI runner for 1C web client automation
|
|
||||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
/**
|
|
||||||
* CLI runner for 1C web client automation.
|
|
||||||
*
|
|
||||||
* Architecture: `start` launches browser + HTTP server in one process.
|
|
||||||
* `exec`, `shot`, `stop` send requests to the running server.
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* node src/run.mjs start <url> — launch browser, connect to 1C, serve requests
|
|
||||||
* node src/run.mjs run <url> <file|-> — autonomous: connect, execute script, disconnect
|
|
||||||
* node src/run.mjs exec <file|-> — run script against existing session
|
|
||||||
* node src/run.mjs shot [file] — take screenshot
|
|
||||||
* node src/run.mjs stop — logout + close browser
|
|
||||||
* node src/run.mjs status — check session
|
|
||||||
*/
|
|
||||||
import http from 'http';
|
|
||||||
import * as browser from './browser.mjs';
|
|
||||||
import { readFileSync, writeFileSync, unlinkSync, existsSync } from 'fs';
|
|
||||||
import { resolve, dirname } from 'path';
|
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
||||||
const SESSION_FILE = resolve(__dirname, '..', '.browser-session.json');
|
|
||||||
|
|
||||||
const [,, cmd, ...rawArgs] = process.argv;
|
|
||||||
const flags = { noRecord: rawArgs.includes('--no-record') };
|
|
||||||
const args = rawArgs.filter(a => !a.startsWith('--'));
|
|
||||||
|
|
||||||
switch (cmd) {
|
|
||||||
case 'start': await cmdStart(args[0]); break;
|
|
||||||
case 'run': await cmdRun(args[0], args[1]); break;
|
|
||||||
case 'exec': await cmdExec(args[0], flags); break;
|
|
||||||
case 'shot': await cmdShot(args[0]); break;
|
|
||||||
case 'stop': await cmdStop(); break;
|
|
||||||
case 'status': cmdStatus(); break;
|
|
||||||
default: usage();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// start: launch browser + HTTP server
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
async function cmdStart(url) {
|
|
||||||
if (!url) die('Usage: node src/run.mjs start <url>');
|
|
||||||
|
|
||||||
// Connect to 1C
|
|
||||||
const state = await browser.connect(url);
|
|
||||||
|
|
||||||
// Start HTTP server for exec/shot/stop
|
|
||||||
const httpServer = http.createServer(handleRequest);
|
|
||||||
httpServer.listen(0, '127.0.0.1', () => {
|
|
||||||
const port = httpServer.address().port;
|
|
||||||
const session = {
|
|
||||||
port,
|
|
||||||
url,
|
|
||||||
pid: process.pid,
|
|
||||||
startedAt: new Date().toISOString()
|
|
||||||
};
|
|
||||||
writeFileSync(SESSION_FILE, JSON.stringify(session, null, 2));
|
|
||||||
out({ ok: true, message: 'Browser ready', port, ...state });
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on('SIGINT', async () => {
|
|
||||||
await browser.disconnect();
|
|
||||||
cleanup();
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleRequest(req, res) {
|
|
||||||
try {
|
|
||||||
if (req.method === 'POST' && req.url === '/exec') {
|
|
||||||
const code = await readBody(req);
|
|
||||||
const noRecord = req.headers['x-no-record'] === '1';
|
|
||||||
const result = await executeScript(code, { noRecord });
|
|
||||||
json(res, result);
|
|
||||||
|
|
||||||
} else if (req.method === 'GET' && req.url === '/shot') {
|
|
||||||
const png = await browser.screenshot();
|
|
||||||
res.writeHead(200, { 'Content-Type': 'image/png' });
|
|
||||||
res.end(png);
|
|
||||||
|
|
||||||
} else if (req.method === 'POST' && req.url === '/stop') {
|
|
||||||
json(res, { ok: true, message: 'Stopping' });
|
|
||||||
await browser.disconnect();
|
|
||||||
cleanup();
|
|
||||||
process.exit(0);
|
|
||||||
|
|
||||||
} else if (req.method === 'GET' && req.url === '/status') {
|
|
||||||
json(res, { ok: true, connected: browser.isConnected() });
|
|
||||||
|
|
||||||
} else {
|
|
||||||
res.writeHead(404);
|
|
||||||
res.end('Not found');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
json(res, { ok: false, error: e.message }, 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function executeScript(code, { noRecord } = {}) {
|
|
||||||
const output = [];
|
|
||||||
const origLog = console.log;
|
|
||||||
const origErr = console.error;
|
|
||||||
console.log = (...a) => output.push(a.map(String).join(' '));
|
|
||||||
console.error = (...a) => output.push('[ERR] ' + a.map(String).join(' '));
|
|
||||||
|
|
||||||
const t0 = Date.now();
|
|
||||||
try {
|
|
||||||
// Build sandbox: all browser.mjs exports + useful Node globals
|
|
||||||
const exports = {};
|
|
||||||
for (const [k, v] of Object.entries(browser)) {
|
|
||||||
if (k !== 'default') exports[k] = v;
|
|
||||||
}
|
|
||||||
exports.writeFileSync = writeFileSync;
|
|
||||||
exports.readFileSync = readFileSync;
|
|
||||||
|
|
||||||
// --no-record: stub recording/narration functions to return safe defaults
|
|
||||||
if (noRecord) {
|
|
||||||
const noop = async () => {};
|
|
||||||
exports.startRecording = noop;
|
|
||||||
exports.stopRecording = async () => ({ file: null, duration: 0, size: 0 });
|
|
||||||
exports.addNarration = async () => ({ file: null, duration: 0, size: 0, captions: 0 });
|
|
||||||
for (const fn of ['showCaption', 'hideCaption']) {
|
|
||||||
exports[fn] = noop;
|
|
||||||
}
|
|
||||||
exports.isRecording = () => false;
|
|
||||||
exports.getCaptions = () => [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wrap action functions to auto-detect 1C errors (modal, balloon)
|
|
||||||
// and stop execution immediately with diagnostic info
|
|
||||||
const ACTION_FNS = [
|
|
||||||
'clickElement', 'fillFields', 'fillField', 'selectValue', 'fillTableRow',
|
|
||||||
'deleteTableRow', 'openCommand', 'navigateSection', 'navigateLink', 'openFile',
|
|
||||||
'closeForm', 'filterList', 'unfilterList'
|
|
||||||
];
|
|
||||||
for (const name of ACTION_FNS) {
|
|
||||||
if (typeof exports[name] !== 'function') continue;
|
|
||||||
const orig = exports[name];
|
|
||||||
exports[name] = async (...args) => {
|
|
||||||
const result = await orig(...args);
|
|
||||||
const errors = result?.errors;
|
|
||||||
if (errors?.modal || errors?.balloon) {
|
|
||||||
// Screenshot while the error modal is still visible (before fetchErrorStack closes it)
|
|
||||||
let errorShot;
|
|
||||||
try {
|
|
||||||
const png = await exports.screenshot();
|
|
||||||
errorShot = resolve(__dirname, '..', 'error-shot.png');
|
|
||||||
writeFileSync(errorShot, png);
|
|
||||||
} catch {}
|
|
||||||
// Try to fetch call stack for modal errors before throwing
|
|
||||||
let stack = null;
|
|
||||||
if (errors?.modal && typeof exports.fetchErrorStack === 'function') {
|
|
||||||
try {
|
|
||||||
stack = await exports.fetchErrorStack(errors.modal.formNum, errors.modal.hasReport);
|
|
||||||
} catch { /* don't fail if stack fetch fails */ }
|
|
||||||
}
|
|
||||||
const msg = errors.modal?.message || errors.balloon?.message || 'Unknown 1C error';
|
|
||||||
const err = new Error(msg);
|
|
||||||
err.onecError = { step: name, args, errors, formState: result, stack, screenshot: errorShot };
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
|
|
||||||
const fn = new AsyncFunction(...Object.keys(exports), code);
|
|
||||||
await fn(...Object.values(exports));
|
|
||||||
|
|
||||||
console.log = origLog;
|
|
||||||
console.error = origErr;
|
|
||||||
return { ok: true, output: output.join('\n'), elapsed: elapsed(t0) };
|
|
||||||
} catch (e) {
|
|
||||||
console.log = origLog;
|
|
||||||
console.error = origErr;
|
|
||||||
|
|
||||||
// Auto-stop recording if active (prevents "Already recording" on next exec)
|
|
||||||
if (browser.isRecording()) {
|
|
||||||
try { await browser.stopRecording(); } catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error screenshot (skip if already taken before fetchErrorStack closed the modal)
|
|
||||||
let shotFile = e.onecError?.screenshot;
|
|
||||||
if (!shotFile) {
|
|
||||||
try {
|
|
||||||
const png = await browser.screenshot();
|
|
||||||
shotFile = resolve(__dirname, '..', 'error-shot.png');
|
|
||||||
writeFileSync(shotFile, png);
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = { ok: false, error: e.message, output: output.join('\n'), screenshot: shotFile, elapsed: elapsed(t0) };
|
|
||||||
|
|
||||||
// Enrich with 1C error context if available
|
|
||||||
if (e.onecError) {
|
|
||||||
result.step = e.onecError.step;
|
|
||||||
result.stepArgs = e.onecError.args;
|
|
||||||
result.onecErrors = e.onecError.errors;
|
|
||||||
result.formState = e.onecError.formState;
|
|
||||||
if (e.onecError.stack) result.stack = e.onecError.stack;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// run: autonomous connect → execute → disconnect (no server)
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
async function cmdRun(url, fileOrDash) {
|
|
||||||
if (!url || !fileOrDash) die('Usage: node src/run.mjs run <url> <file|->');
|
|
||||||
|
|
||||||
const code = fileOrDash === '-'
|
|
||||||
? await readStdin()
|
|
||||||
: readFileSync(resolve(fileOrDash), 'utf-8');
|
|
||||||
|
|
||||||
await browser.connect(url);
|
|
||||||
const result = await executeScript(code);
|
|
||||||
await browser.disconnect();
|
|
||||||
|
|
||||||
out(result);
|
|
||||||
if (!result.ok) process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// exec: send script to running server
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
async function cmdExec(fileOrDash, flags = {}) {
|
|
||||||
if (!fileOrDash) die('Usage: node src/run.mjs exec <file|-> [--no-record]');
|
|
||||||
|
|
||||||
let code = fileOrDash === '-'
|
|
||||||
? await readStdin()
|
|
||||||
: readFileSync(resolve(fileOrDash), 'utf-8');
|
|
||||||
|
|
||||||
const sess = loadSession();
|
|
||||||
const headers = {};
|
|
||||||
if (flags.noRecord) headers['x-no-record'] = '1';
|
|
||||||
const result = await new Promise((resolve, reject) => {
|
|
||||||
const req = http.request({
|
|
||||||
hostname: '127.0.0.1', port: sess.port, path: '/exec',
|
|
||||||
method: 'POST', timeout: 30 * 60 * 1000, headers,
|
|
||||||
}, res => {
|
|
||||||
let data = '';
|
|
||||||
res.on('data', chunk => data += chunk);
|
|
||||||
res.on('end', () => { try { resolve(JSON.parse(data)); } catch { reject(new Error(data)); } });
|
|
||||||
});
|
|
||||||
req.on('error', reject);
|
|
||||||
req.on('timeout', () => { req.destroy(new Error('Exec timeout (10 min)')); });
|
|
||||||
req.write(code);
|
|
||||||
req.end();
|
|
||||||
});
|
|
||||||
out(result);
|
|
||||||
if (!result.ok) process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// shot: take screenshot via server
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
async function cmdShot(file) {
|
|
||||||
const sess = loadSession();
|
|
||||||
const resp = await fetch(`http://127.0.0.1:${sess.port}/shot`);
|
|
||||||
if (!resp.ok) {
|
|
||||||
const err = await resp.text();
|
|
||||||
die(`Screenshot failed: ${err}`);
|
|
||||||
}
|
|
||||||
const buf = Buffer.from(await resp.arrayBuffer());
|
|
||||||
const outFile = file || 'shot.png';
|
|
||||||
writeFileSync(outFile, buf);
|
|
||||||
out({ ok: true, file: outFile });
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// stop: send stop to server
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
async function cmdStop() {
|
|
||||||
const sess = loadSession();
|
|
||||||
try {
|
|
||||||
const resp = await fetch(`http://127.0.0.1:${sess.port}/stop`, { method: 'POST' });
|
|
||||||
const result = await resp.json();
|
|
||||||
out(result);
|
|
||||||
} catch {
|
|
||||||
// Server may have already exited before responding
|
|
||||||
out({ ok: true, message: 'Stopped' });
|
|
||||||
}
|
|
||||||
cleanup();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// status: check session
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
function cmdStatus() {
|
|
||||||
if (!existsSync(SESSION_FILE)) {
|
|
||||||
out({ ok: false, message: 'No active session' });
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
const sess = JSON.parse(readFileSync(SESSION_FILE, 'utf-8'));
|
|
||||||
out({ ok: true, ...sess });
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// helpers
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
function loadSession() {
|
|
||||||
if (!existsSync(SESSION_FILE)) {
|
|
||||||
die('No active session. Run: node src/run.mjs start <url>');
|
|
||||||
}
|
|
||||||
return JSON.parse(readFileSync(SESSION_FILE, 'utf-8'));
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanup() {
|
|
||||||
try { unlinkSync(SESSION_FILE); } catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function readBody(req) {
|
|
||||||
const chunks = [];
|
|
||||||
for await (const chunk of req) chunks.push(chunk);
|
|
||||||
return Buffer.concat(chunks).toString('utf-8');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function readStdin() {
|
|
||||||
const chunks = [];
|
|
||||||
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
||||||
return Buffer.concat(chunks).toString('utf-8');
|
|
||||||
}
|
|
||||||
|
|
||||||
function elapsed(t0) {
|
|
||||||
return Math.round((Date.now() - t0) / 100) / 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
function json(res, obj, status = 200) {
|
|
||||||
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
||||||
res.end(JSON.stringify(obj, null, 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
function out(obj) {
|
|
||||||
process.stdout.write(JSON.stringify(obj, null, 2) + '\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
function die(msg) {
|
|
||||||
process.stderr.write(msg + '\n');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function usage() {
|
|
||||||
die(`Usage: node src/run.mjs <command> [args]
|
|
||||||
|
|
||||||
Commands:
|
|
||||||
start <url> Launch browser and connect to 1C web client
|
|
||||||
run <url> <file|-> Autonomous: connect, execute script, disconnect
|
|
||||||
exec <file|-> [options] Execute script (file path or - for stdin)
|
|
||||||
shot [file] Take screenshot (default: shot.png)
|
|
||||||
stop Logout and close browser
|
|
||||||
status Check session status
|
|
||||||
|
|
||||||
Options for exec:
|
|
||||||
--no-record Skip video recording (record() becomes no-op)`);
|
|
||||||
}
|
|
||||||
-48
@@ -1,48 +0,0 @@
|
|||||||
# Реальные выгрузки обработок (примеры, не для версионирования)
|
|
||||||
upload/
|
|
||||||
|
|
||||||
# Результаты сборки
|
|
||||||
build/
|
|
||||||
base/
|
|
||||||
*.epf
|
|
||||||
*.log
|
|
||||||
|
|
||||||
# Временные файлы тестов
|
|
||||||
test-tmp/
|
|
||||||
|
|
||||||
# Локальные настройки Claude Code
|
|
||||||
.claude/settings.local.json
|
|
||||||
|
|
||||||
# Инструменты (portable Apache и т.д.)
|
|
||||||
tools/
|
|
||||||
|
|
||||||
# Отладка навыков (eval, trigger-test, run_loop результаты)
|
|
||||||
debug/
|
|
||||||
|
|
||||||
# Python кэш
|
|
||||||
__pycache__/
|
|
||||||
|
|
||||||
# Локальный реестр баз данных 1С
|
|
||||||
.v8-project.json
|
|
||||||
|
|
||||||
# web-test: Node.js зависимости и runtime-артефакты
|
|
||||||
.claude/skills/web-test/scripts/node_modules/
|
|
||||||
.claude/skills/web-test/.browser-session.json
|
|
||||||
|
|
||||||
# Скриншоты и видео (артефакты тестирования web-test)
|
|
||||||
*.png
|
|
||||||
*.mp4
|
|
||||||
|
|
||||||
# Навыки, скопированные для других AI-платформ (генерируются scripts/switch.py)
|
|
||||||
.agents/
|
|
||||||
.augment/
|
|
||||||
.cline/
|
|
||||||
.codex/
|
|
||||||
.cursor/
|
|
||||||
.gemini/
|
|
||||||
.github/skills/
|
|
||||||
.kilocode/
|
|
||||||
.kiro/
|
|
||||||
.opencode/
|
|
||||||
.roo/
|
|
||||||
.windsurf/
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: cf-edit
|
name: cf-edit
|
||||||
description: Точечное редактирование конфигурации 1С. Используй когда нужно изменить свойства конфигурации, добавить или удалить объект из состава, настроить роли по умолчанию
|
description: Точечное редактирование конфигурации 1С. Используй когда нужно изменить свойства конфигурации, добавить или удалить объект из состава, настроить роли по умолчанию, поменять раскладку панелей, настроить начальную страницу
|
||||||
argument-hint: -ConfigPath <path> -Operation <op> -Value <value>
|
argument-hint: -ConfigPath <path> -Operation <op> -Value <value>
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash
|
- Bash
|
||||||
@@ -24,7 +24,7 @@ allowed-tools:
|
|||||||
| `NoValidate` | Пропустить авто-валидацию |
|
| `NoValidate` | Пропустить авто-валидацию |
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File .claude/skills/cf-edit/scripts/cf-edit.ps1 -ConfigPath '<path>' -Operation modify-property -Value 'Version=1.0.0.1'
|
python ".opencode/skills/cf-edit/scripts/cf-edit.py" -ConfigPath '<path>' -Operation modify-property -Value 'Version=1.0.0.1'
|
||||||
```
|
```
|
||||||
|
|
||||||
## Операции
|
## Операции
|
||||||
@@ -32,27 +32,29 @@ powershell.exe -NoProfile -File .claude/skills/cf-edit/scripts/cf-edit.ps1 -Conf
|
|||||||
| Операция | Формат Value | Описание |
|
| Операция | Формат Value | Описание |
|
||||||
|----------|-------------|----------|
|
|----------|-------------|----------|
|
||||||
| `modify-property` | `Ключ=Значение` (batch `;;`) | Изменить свойство |
|
| `modify-property` | `Ключ=Значение` (batch `;;`) | Изменить свойство |
|
||||||
| `add-childObject` | `Type.Name` (batch `;;`) | Добавить объект в ChildObjects |
|
| `add-childObject` | `Type.Name` (batch `;;`) | Зарегистрировать уже существующий файл объекта в ChildObjects. Для создания нового объекта используй `/meta-compile`, `/role-compile`, `/subsystem-compile` — они регистрируют автоматически |
|
||||||
| `remove-childObject` | `Type.Name` (batch `;;`) | Удалить объект из ChildObjects |
|
| `remove-childObject` | `Type.Name` (batch `;;`) | Удалить объект из ChildObjects |
|
||||||
| `add-defaultRole` | `Role.Name` или `Name` | Добавить роль по умолчанию |
|
| `add-defaultRole` | `Role.Name` или `Name` | Добавить роль по умолчанию |
|
||||||
| `remove-defaultRole` | `Role.Name` или `Name` | Удалить роль по умолчанию |
|
| `remove-defaultRole` | `Role.Name` или `Name` | Удалить роль по умолчанию |
|
||||||
| `set-defaultRoles` | Имена через `;;` | Заменить список ролей по умолчанию |
|
| `set-defaultRoles` | Имена через `;;` | Заменить список ролей по умолчанию |
|
||||||
|
| `set-panels` | JSON-объект (см. [reference.md](reference.md)) | Перезаписать `Ext/ClientApplicationInterface.xml` (раскладка панелей) |
|
||||||
|
| `set-home-page` | JSON-объект (см. [reference.md](reference.md)) | Перезаписать `Ext/HomePageWorkArea.xml` (начальная страница) |
|
||||||
|
|
||||||
Подробнее: `reference.md` в каталоге навыка.
|
Допустимые значения свойств, формат DefinitionFile (JSON), каноничный порядок: [reference.md](reference.md)
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Изменить версию и поставщика
|
# Изменить версию и поставщика
|
||||||
... -ConfigPath test-tmp/cf -Operation modify-property -Value "Version=1.0.0.1 ;; Vendor=Фирма 1С"
|
... -ConfigPath src -Operation modify-property -Value "Version=1.0.0.1 ;; Vendor=Фирма 1С"
|
||||||
|
|
||||||
# Добавить объекты
|
# Добавить объекты
|
||||||
... -ConfigPath test-tmp/cf -Operation add-childObject -Value "Catalog.Товары ;; Document.Заказ"
|
... -ConfigPath src -Operation add-childObject -Value "Catalog.Товары ;; Document.Заказ"
|
||||||
|
|
||||||
# Удалить объект
|
# Удалить объект
|
||||||
... -ConfigPath test-tmp/cf -Operation remove-childObject -Value "Catalog.Устаревший"
|
... -ConfigPath src -Operation remove-childObject -Value "Catalog.Устаревший"
|
||||||
|
|
||||||
# Роли по умолчанию
|
# Роли по умолчанию
|
||||||
... -ConfigPath test-tmp/cf -Operation add-defaultRole -Value "ПолныеПрава"
|
... -ConfigPath src -Operation add-defaultRole -Value "ПолныеПрава"
|
||||||
... -ConfigPath test-tmp/cf -Operation set-defaultRoles -Value "ПолныеПрава ;; Администратор"
|
... -ConfigPath src -Operation set-defaultRoles -Value "ПолныеПрава ;; Администратор"
|
||||||
```
|
```
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
# cf-edit — справочник операций
|
||||||
|
|
||||||
|
## modify-property
|
||||||
|
|
||||||
|
Свойства для редактирования:
|
||||||
|
|
||||||
|
### Скалярные
|
||||||
|
`Name`, `Version`, `Vendor`, `Comment`, `NamePrefix`, `UpdateCatalogAddress`
|
||||||
|
|
||||||
|
### LocalString (многоязычные)
|
||||||
|
`Synonym`, `BriefInformation`, `DetailedInformation`, `Copyright`, `VendorInformationAddress`, `ConfigurationInformationAddress`
|
||||||
|
|
||||||
|
### Enum
|
||||||
|
| Свойство | Допустимые значения |
|
||||||
|
|----------|---------------------|
|
||||||
|
| `CompatibilityMode` | `Version8_3_20` ... `Version8_3_28`, `Version8_5_1`, `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` | `Version8_2`, `Version8_2EnableTaxi`, `Taxi`, `TaxiEnableVersion8_2`, `TaxiEnableVersion8_5`, `Version8_5EnableTaxi`, `Version8_5` |
|
||||||
|
| `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-тип и имя объекта через точку.
|
||||||
|
|
||||||
|
**Важно про `add-childObject`**: регистрирует в `<ChildObjects>` объект, **файл которого уже существует на диске**. Если файла нет — exit 1. Для создания нового объекта используй профильный навык — `/meta-compile` (Catalog, Document, Enum, Report, регистры и т.д.), `/role-compile` (Role), `/subsystem-compile` (Subsystem). Они создают файл И регистрируют его за один вызов.
|
||||||
|
|
||||||
|
Batch: `"Catalog.Товары ;; Document.Заказ ;; Enum.ВидыОплат"`
|
||||||
|
|
||||||
|
## add-defaultRole / remove-defaultRole / set-defaultRoles
|
||||||
|
|
||||||
|
Имя роли: `ПолныеПрава` или `Role.ПолныеПрава` (префикс `Role.` добавляется автоматически).
|
||||||
|
|
||||||
|
`set-defaultRoles` полностью заменяет список ролей.
|
||||||
|
|
||||||
|
## set-panels
|
||||||
|
|
||||||
|
Перезаписывает `Ext/ClientApplicationInterface.xml` — раскладку панелей рабочего пространства Taxi. Файл создаётся с нуля; то, что не упомянуто в `value`, отсутствует на экране.
|
||||||
|
|
||||||
|
`value` — объект с ключами `top`, `left`, `right`, `bottom`. Каждый ключ — массив записей. Ключ можно опустить (= пустая сторона).
|
||||||
|
|
||||||
|
**Запись** — одна из:
|
||||||
|
- Строка-алиас (одна панель в этом слоте)
|
||||||
|
- Объект `{"group": [...]}` (стек: панели/подгруппы внутри располагаются друг под другом)
|
||||||
|
|
||||||
|
**Алиасы панелей:**
|
||||||
|
|
||||||
|
| Алиас | Панель |
|
||||||
|
|-------|--------|
|
||||||
|
| `sections` | Панель разделов |
|
||||||
|
| `open` | Панель открытых |
|
||||||
|
| `favorites` | Панель избранного |
|
||||||
|
| `history` | Панель истории |
|
||||||
|
| `functions` | Панель функций текущего раздела |
|
||||||
|
|
||||||
|
**Семантика:**
|
||||||
|
- Несколько записей в одной стороне → отдельные слоты «рядом» (несколько тегов `<top>`/...)
|
||||||
|
- `{"group":[...]}` → один тег с `<group>`-обёрткой, элементы внутри идут стеком
|
||||||
|
|
||||||
|
**Пример** (DefinitionFile):
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"operation": "set-panels",
|
||||||
|
"value": {
|
||||||
|
"top": ["open"],
|
||||||
|
"left": ["sections"],
|
||||||
|
"right": [{ "group": ["favorites", "history"] }],
|
||||||
|
"bottom": ["functions"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Через `-Value` (CLI): передай объект как JSON-строку — `... -Operation set-panels -Value '{"top":["open"]}'`.
|
||||||
|
|
||||||
|
## set-home-page
|
||||||
|
|
||||||
|
Перезаписывает `Ext/HomePageWorkArea.xml` — раскладка форм на начальной странице (рабочая область). Файл создаётся с нуля; то, что не упомянуто в `value`, отсутствует.
|
||||||
|
|
||||||
|
`value` — объект:
|
||||||
|
|
||||||
|
| Ключ | Канонич. (XML) | Описание |
|
||||||
|
|------|----------------|----------|
|
||||||
|
| `template` | `WorkingAreaTemplate` | `OneColumn` / `TwoColumnsEqualWidth` (дефолт) / `TwoColumnsVariableWidth` |
|
||||||
|
| `left` | `LeftColumn` | массив записей форм |
|
||||||
|
| `right` | `RightColumn` | массив записей форм (запрещён при `OneColumn`) |
|
||||||
|
|
||||||
|
Принимаются и короткие и канонич. ключи (XML-имена) — оба работают.
|
||||||
|
|
||||||
|
**Запись формы** — одна из:
|
||||||
|
- Строка `"<form>"` — только имя формы, дефолты `height=10`, `visibility=true`
|
||||||
|
- Объект `{form, height?, visibility?, roles?}`
|
||||||
|
|
||||||
|
| Поле | Канонич. | Дефолт | Описание |
|
||||||
|
|------|----------|--------|----------|
|
||||||
|
| `form` | `Form` | — | `CommonForm.X` или `Type.Object.Form.Name` (или UUID) |
|
||||||
|
| `height` | `Height` | `10` | Высота |
|
||||||
|
| `visibility` | `Visibility` | `true` | Общая видимость (`<xr:Common>`) |
|
||||||
|
| `roles` | — | — | `{"Role.Имя": true|false, ...}` — переопределения по ролям |
|
||||||
|
|
||||||
|
**Семантика visibility:** `visibility` = общее правило, `roles` — точечные исключения. Скрыть для всех кроме одной роли: `{"visibility": false, "roles": {"Role.Опер": true}}`.
|
||||||
|
|
||||||
|
**Пример:**
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"operation": "set-home-page",
|
||||||
|
"value": {
|
||||||
|
"template": "TwoColumnsVariableWidth",
|
||||||
|
"left": [
|
||||||
|
"CommonForm.НачалоРаботы",
|
||||||
|
{ "form": "CommonForm.СписокЗадач", "height": 100, "visibility": false },
|
||||||
|
{ "form": "Catalog.Контрагенты.Form.ФормаСписка", "height": 50 },
|
||||||
|
{
|
||||||
|
"form": "CommonForm.РабочийСтолОператора",
|
||||||
|
"visibility": false,
|
||||||
|
"roles": { "Role.Оператор": true, "Role.ПолныеПрава": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"right": [
|
||||||
|
{ "form": "DataProcessor.Поиск.Form.ФормаПоиска", "height": 30 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## DefinitionFile (JSON)
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{ "operation": "modify-property", "value": "Version=2.0.0.1 ;; Vendor=Test" },
|
||||||
|
{ "operation": "add-childObject", "value": "Catalog.Товары ;; Document.Заказ" },
|
||||||
|
{ "operation": "add-defaultRole", "value": "ПолныеПрава" }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
@@ -0,0 +1,989 @@
|
|||||||
|
# cf-edit v1.7 — Edit 1C configuration root (Configuration.xml)
|
||||||
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)][Alias('Path')][string]$ConfigPath,
|
||||||
|
[string]$DefinitionFile,
|
||||||
|
[ValidateSet("modify-property","add-childObject","remove-childObject","add-defaultRole","remove-defaultRole","set-defaultRoles","set-panels","set-home-page")]
|
||||||
|
[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
|
||||||
|
$script:configDir = [System.IO.Path]::GetDirectoryName($resolvedPath)
|
||||||
|
|
||||||
|
# --- Support guard (Ext/ParentConfigurations.bin) ---
|
||||||
|
# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
|
||||||
|
# read-only configs unless allowed. Trigger = bin present; reaction from
|
||||||
|
# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never
|
||||||
|
# throws — guard errors degrade to allow.
|
||||||
|
function Get-RootUuid([string]$xmlPath) {
|
||||||
|
if (-not (Test-Path $xmlPath)) { return $null }
|
||||||
|
try {
|
||||||
|
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||||
|
$el = $mx.DocumentElement.FirstChild
|
||||||
|
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||||
|
if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
|
||||||
|
} catch {}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
function Find-V8Project([string]$startDir) {
|
||||||
|
$d = $startDir
|
||||||
|
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||||
|
$pj = Join-Path $d ".v8-project.json"
|
||||||
|
if (Test-Path $pj) { return $pj }
|
||||||
|
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||||
|
if ($parent -eq $d) { break }
|
||||||
|
$d = $parent
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
function Get-EditMode([string]$cfgDir) {
|
||||||
|
try {
|
||||||
|
$pj = Find-V8Project (Get-Location).Path
|
||||||
|
if (-not $pj) { $pj = Find-V8Project $cfgDir }
|
||||||
|
if (-not $pj) { return 'deny' }
|
||||||
|
$proj = Get-Content -Raw $pj | ConvertFrom-Json
|
||||||
|
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
|
||||||
|
if ($proj.databases) {
|
||||||
|
foreach ($db in $proj.databases) {
|
||||||
|
if ($db.configSrc) {
|
||||||
|
$src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/')
|
||||||
|
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
|
||||||
|
if ($db.editingAllowedCheck) { return $db.editingAllowedCheck }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck }
|
||||||
|
return 'deny'
|
||||||
|
} catch { return 'deny' }
|
||||||
|
}
|
||||||
|
function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
||||||
|
try {
|
||||||
|
$rp = $targetPath
|
||||||
|
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
||||||
|
$elemUuid = Get-RootUuid $rp
|
||||||
|
$cfgDir = $null; $binPath = $null
|
||||||
|
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
||||||
|
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||||
|
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||||
|
if (-not $cfgDir) {
|
||||||
|
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||||
|
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand }
|
||||||
|
}
|
||||||
|
if ($elemUuid -and $cfgDir) { break }
|
||||||
|
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||||
|
if ($parent -eq $d) { break }
|
||||||
|
$d = $parent
|
||||||
|
}
|
||||||
|
# New object (no element file): fall back to config root uuid.
|
||||||
|
if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") }
|
||||||
|
if (-not $binPath -or -not (Test-Path $binPath)) { return }
|
||||||
|
$bytes = [System.IO.File]::ReadAllBytes($binPath)
|
||||||
|
if ($bytes.Length -le 32) { return }
|
||||||
|
$start = 0
|
||||||
|
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
|
||||||
|
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
|
||||||
|
$hm = [regex]::Match($text, '^\{6,(\d+),(\d+),')
|
||||||
|
if (-not $hm.Success) { return }
|
||||||
|
$G = [int]$hm.Groups[1].Value
|
||||||
|
$K = [int]$hm.Groups[2].Value
|
||||||
|
if ($K -eq 0) { return }
|
||||||
|
$best = $null
|
||||||
|
if ($elemUuid) {
|
||||||
|
$u = [regex]::Escape($elemUuid.ToLower())
|
||||||
|
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
|
||||||
|
$f1 = [int]$m.Groups[1].Value
|
||||||
|
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$blocked = $false; $code = ""; $reason = ""
|
||||||
|
if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" }
|
||||||
|
elseif ($require -eq 'removed') {
|
||||||
|
if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" }
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" }
|
||||||
|
}
|
||||||
|
if (-not $blocked) { return }
|
||||||
|
$mode = Get-EditMode $cfgDir
|
||||||
|
if ($mode -eq 'off') { return }
|
||||||
|
# Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the
|
||||||
|
# latter throws and would be swallowed by this function's own catch.
|
||||||
|
if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return }
|
||||||
|
$head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
|
||||||
|
$cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
|
||||||
|
$offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
|
||||||
|
if ($code -eq "capability-off") {
|
||||||
|
$state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя."
|
||||||
|
$fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
|
||||||
|
} elseif ($code -eq "not-removed") {
|
||||||
|
$state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
|
||||||
|
$fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно."
|
||||||
|
} else {
|
||||||
|
$state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
|
||||||
|
$fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят."
|
||||||
|
}
|
||||||
|
[Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote")
|
||||||
|
exit 1
|
||||||
|
} catch { return }
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert-EditAllowed $resolvedPath 'editable'
|
||||||
|
|
||||||
|
# --- 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 <Configuration> 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"
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- Type → on-disk directory name (plural) ---
|
||||||
|
$script:typeToDir = @{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 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</_W>"
|
||||||
|
$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<v8:item>`r`n$indent`t`t<v8:lang>ru</v8:lang>`r`n$indent`t`t<v8:content>$escaped</v8:content>`r`n$indent`t</v8:item>`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 <ChildObjects> 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
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check that the referenced object actually exists on disk.
|
||||||
|
# cf-edit add-childObject is a low-level operation for rare scenarios
|
||||||
|
# (e.g. restoring a rolled-back Configuration.xml when object files are intact).
|
||||||
|
# For creating NEW objects, meta-compile/role-compile/subsystem-compile already
|
||||||
|
# auto-register in Configuration.xml — calling cf-edit add-childObject there is
|
||||||
|
# unnecessary and error-prone.
|
||||||
|
$typeDir = $script:typeToDir[$typeName]
|
||||||
|
$objFile = Join-Path (Join-Path $script:configDir $typeDir) "$objNameVal.xml"
|
||||||
|
if (-not (Test-Path $objFile)) {
|
||||||
|
$hintSkill = switch ($typeName) {
|
||||||
|
"Subsystem" { "subsystem-compile" }
|
||||||
|
"Role" { "role-compile" }
|
||||||
|
default { "meta-compile" }
|
||||||
|
}
|
||||||
|
Write-Error @"
|
||||||
|
Object file not found: $typeDir/$objNameVal.xml
|
||||||
|
cf-edit add-childObject only references objects that already exist on disk.
|
||||||
|
To create a new $typeName, use $hintSkill (auto-registers in Configuration.xml):
|
||||||
|
/$hintSkill with {"type":"$typeName","name":"$objNameVal"}
|
||||||
|
"@
|
||||||
|
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 <ChildObjects> 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 <DefaultRoles> 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 = "<xr:Item xsi:type=`"xr:MDObjectRef`">$roleName</xr:Item>"
|
||||||
|
$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 <DefaultRoles> 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-panels ---
|
||||||
|
# Canonical English aliases — preferred form, used in docs and error messages.
|
||||||
|
$script:panelUuids = @{
|
||||||
|
"sections" = "b553047f-c9aa-4157-978d-448ecad24248"
|
||||||
|
"open" = "cbab57f2-a0f3-4f0a-89ea-4cb19570ab75"
|
||||||
|
"favorites" = "13322b22-3960-4d68-93a6-fe2dd7f28ca3"
|
||||||
|
"history" = "c933ac92-92cd-459d-81cc-e0c8a83ced99"
|
||||||
|
"functions" = "b2735bd3-d822-4430-ba59-c9e869693b24"
|
||||||
|
}
|
||||||
|
# Russian synonyms — silently accepted (cf-info displays Russian names; users
|
||||||
|
# may copy them straight into cf-edit value).
|
||||||
|
$script:panelSynonyms = @{
|
||||||
|
"разделов" = "sections"; "разделы" = "sections"
|
||||||
|
"открытых" = "open"; "открытые" = "open"
|
||||||
|
"избранного" = "favorites";"избранное" = "favorites"
|
||||||
|
"истории" = "history"; "история" = "history"
|
||||||
|
"функций" = "functions";"функции" = "functions"
|
||||||
|
}
|
||||||
|
|
||||||
|
function Build-PanelEntryXml($entry, [string]$indent) {
|
||||||
|
# String alias -> <panel><uuid>...</uuid></panel>
|
||||||
|
if ($entry -is [string]) {
|
||||||
|
$key = $entry.ToLowerInvariant()
|
||||||
|
if ($script:panelSynonyms.ContainsKey($key)) { $key = $script:panelSynonyms[$key] }
|
||||||
|
if (-not $script:panelUuids.ContainsKey($key)) {
|
||||||
|
Write-Error "Unknown panel alias '$entry'. Allowed: $(($script:panelUuids.Keys | Sort-Object) -join ', ')"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
$u = $script:panelUuids[$key]
|
||||||
|
$instId = [guid]::NewGuid().ToString()
|
||||||
|
return "$indent<panel id=`"$instId`">`r`n$indent`t<uuid>$u</uuid>`r`n$indent</panel>"
|
||||||
|
}
|
||||||
|
# Object {group: [...]} -> <group id=""><group><panel/></group>...</group> (stack)
|
||||||
|
if ($entry.PSObject.Properties['group']) {
|
||||||
|
$children = $entry.group
|
||||||
|
if (-not $children -or $children.Count -eq 0) {
|
||||||
|
Write-Error "group must contain at least one entry"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
$gid = [guid]::NewGuid().ToString()
|
||||||
|
$inner = ""
|
||||||
|
foreach ($child in $children) {
|
||||||
|
$childXml = Build-PanelEntryXml $child "$indent`t`t"
|
||||||
|
$inner += "$indent`t<group>`r`n$childXml`r`n$indent`t</group>`r`n"
|
||||||
|
}
|
||||||
|
return "$indent<group id=`"$gid`">`r`n$inner$indent</group>"
|
||||||
|
}
|
||||||
|
Write-Error "Panel entry must be a string alias or object {group:[...]}, got: $($entry | ConvertTo-Json -Compress)"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function Do-SetPanels($valArg) {
|
||||||
|
# Accept string (JSON), PSCustomObject, or hashtable
|
||||||
|
$layout = $valArg
|
||||||
|
if ($layout -is [string]) {
|
||||||
|
try { $layout = $layout | ConvertFrom-Json } catch {
|
||||||
|
Write-Error "set-panels value must be valid JSON object, got: $valArg"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (-not $layout) {
|
||||||
|
Write-Error "set-panels value is empty"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$sides = @("top","left","right","bottom")
|
||||||
|
$bodyParts = @()
|
||||||
|
foreach ($side in $sides) {
|
||||||
|
$entries = $null
|
||||||
|
if ($layout.PSObject.Properties[$side]) { $entries = $layout.$side }
|
||||||
|
if ($null -eq $entries) { continue }
|
||||||
|
# Normalize to array
|
||||||
|
if ($entries -isnot [System.Array] -and $entries -isnot [System.Collections.IList]) {
|
||||||
|
$entries = @($entries)
|
||||||
|
}
|
||||||
|
foreach ($entry in $entries) {
|
||||||
|
$entryXml = Build-PanelEntryXml $entry "`t`t"
|
||||||
|
$bodyParts += "`t<$side>`r`n$entryXml`r`n`t</$side>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Reject unknown side keys (catches typos like "Top" vs "top")
|
||||||
|
foreach ($prop in $layout.PSObject.Properties) {
|
||||||
|
if ($sides -notcontains $prop.Name) {
|
||||||
|
Write-Error "Unknown side '$($prop.Name)'. Allowed: $($sides -join ', ')"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = $bodyParts -join "`r`n"
|
||||||
|
$declarations = @"
|
||||||
|
<panelDef id="b553047f-c9aa-4157-978d-448ecad24248"/>
|
||||||
|
<panelDef id="13322b22-3960-4d68-93a6-fe2dd7f28ca3"/>
|
||||||
|
<panelDef id="c933ac92-92cd-459d-81cc-e0c8a83ced99"/>
|
||||||
|
<panelDef id="cbab57f2-a0f3-4f0a-89ea-4cb19570ab75"/>
|
||||||
|
<panelDef id="b2735bd3-d822-4430-ba59-c9e869693b24"/>
|
||||||
|
"@
|
||||||
|
$bodyBlock = if ($body) { "$body`r`n" } else { "" }
|
||||||
|
$caiXml = @"
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ClientApplicationInterface xmlns="http://v8.1c.ru/8.2/managed-application/core" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="InterfaceLayouter">
|
||||||
|
$bodyBlock$declarations
|
||||||
|
</ClientApplicationInterface>
|
||||||
|
"@
|
||||||
|
|
||||||
|
$extDir = Join-Path $script:configDir "Ext"
|
||||||
|
if (-not (Test-Path $extDir)) { New-Item -ItemType Directory -Path $extDir -Force | Out-Null }
|
||||||
|
$caiPath = Join-Path $extDir "ClientApplicationInterface.xml"
|
||||||
|
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||||
|
[System.IO.File]::WriteAllText($caiPath, $caiXml, $utf8Bom)
|
||||||
|
$script:modifyCount++
|
||||||
|
Info "Wrote panel layout: $caiPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Operation: set-home-page ---
|
||||||
|
# Russian → English type aliases for form-ref normalization
|
||||||
|
$script:ruTypeMap = @{
|
||||||
|
"справочник" = "Catalog"
|
||||||
|
"документ" = "Document"
|
||||||
|
"перечисление" = "Enum"
|
||||||
|
"отчёт" = "Report"
|
||||||
|
"отчет" = "Report"
|
||||||
|
"обработка" = "DataProcessor"
|
||||||
|
"общаяформа" = "CommonForm"
|
||||||
|
"журналдокументов" = "DocumentJournal"
|
||||||
|
"планвидовхарактеристик" = "ChartOfCharacteristicTypes"
|
||||||
|
"плансчетов" = "ChartOfAccounts"
|
||||||
|
"планвидоврасчета" = "ChartOfCalculationTypes"
|
||||||
|
"планвидоврасчёта" = "ChartOfCalculationTypes"
|
||||||
|
"регистрсведений" = "InformationRegister"
|
||||||
|
"регистрнакопления" = "AccumulationRegister"
|
||||||
|
"регистрбухгалтерии" = "AccountingRegister"
|
||||||
|
"регистррасчета" = "CalculationRegister"
|
||||||
|
"регистррасчёта" = "CalculationRegister"
|
||||||
|
"бизнеспроцесс" = "BusinessProcess"
|
||||||
|
"задача" = "Task"
|
||||||
|
"планобмена" = "ExchangePlan"
|
||||||
|
"хранилищенастроек" = "SettingsStorage"
|
||||||
|
}
|
||||||
|
# plural folder → singular type
|
||||||
|
$script:dirToType = @{}
|
||||||
|
foreach ($k in $script:typeToDir.Keys) { $script:dirToType[$script:typeToDir[$k].ToLowerInvariant()] = $k }
|
||||||
|
|
||||||
|
function Normalize-FormRef([string]$s) {
|
||||||
|
$s = $s.Trim()
|
||||||
|
if (-not $s) { return $s }
|
||||||
|
# UUID — leave as-is
|
||||||
|
if ($s -match '^[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}$') { return $s }
|
||||||
|
# Path form?
|
||||||
|
if ($s.Contains("/") -or $s.Contains("\")) {
|
||||||
|
$parts = $s.Replace("\","/").Split("/") | Where-Object { $_ -ne "" -and $_.ToLowerInvariant() -ne "ext" }
|
||||||
|
# Strip trailing Form.xml
|
||||||
|
if ($parts.Count -gt 0 -and $parts[-1].ToLowerInvariant() -eq "form.xml") {
|
||||||
|
$parts = @($parts[0..($parts.Count - 2)])
|
||||||
|
}
|
||||||
|
if ($parts.Count -ge 2) {
|
||||||
|
$typeDir = $parts[0]
|
||||||
|
$typeSingular = $script:dirToType[$typeDir.ToLowerInvariant()]
|
||||||
|
if ($typeSingular) {
|
||||||
|
if ($typeSingular -eq "CommonForm" -and $parts.Count -ge 2) {
|
||||||
|
return "CommonForm.$($parts[1])"
|
||||||
|
}
|
||||||
|
if ($parts.Count -ge 4 -and $parts[2].ToLowerInvariant() -eq "forms") {
|
||||||
|
return "$typeSingular.$($parts[1]).Form.$($parts[3])"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $s
|
||||||
|
}
|
||||||
|
# Dot form — translate Russian head and 'Форма' segment, auto-insert 'Form'
|
||||||
|
$segs = $s.Split(".")
|
||||||
|
if ($segs.Count -ge 1) {
|
||||||
|
$head = $segs[0].ToLowerInvariant()
|
||||||
|
if ($script:ruTypeMap.ContainsKey($head)) { $segs[0] = $script:ruTypeMap[$head] }
|
||||||
|
for ($i = 1; $i -lt $segs.Count; $i++) {
|
||||||
|
if ($segs[$i] -eq "Форма") { $segs[$i] = "Form" }
|
||||||
|
}
|
||||||
|
# Auto-insert Form: for object types with 3 segments (Type.Object.FormName)
|
||||||
|
if ($segs.Count -eq 3 -and $script:typeOrder -contains $segs[0] -and $segs[0] -ne "CommonForm") {
|
||||||
|
$segs = @($segs[0], $segs[1], "Form", $segs[2])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ($segs -join ".")
|
||||||
|
}
|
||||||
|
|
||||||
|
# Accept short DSL or canonical XML keys (silently)
|
||||||
|
function Get-FieldValue($obj, [string[]]$keys) {
|
||||||
|
foreach ($k in $keys) {
|
||||||
|
if ($obj.PSObject.Properties[$k]) { return $obj.PSObject.Properties[$k].Value }
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Build-HomePageItemXml($entry, [string]$indent) {
|
||||||
|
# Resolve fields
|
||||||
|
if ($entry -is [string]) {
|
||||||
|
$formRef = Normalize-FormRef $entry
|
||||||
|
$height = 10
|
||||||
|
$common = $true
|
||||||
|
$roles = $null
|
||||||
|
} else {
|
||||||
|
$formRaw = Get-FieldValue $entry @("form","Form")
|
||||||
|
if (-not $formRaw) { Write-Error "Home page item: 'form' is required, got: $($entry | ConvertTo-Json -Compress)"; exit 1 }
|
||||||
|
$formRef = Normalize-FormRef ([string]$formRaw)
|
||||||
|
$h = Get-FieldValue $entry @("height","Height")
|
||||||
|
$height = if ($null -ne $h) { [int]$h } else { 10 }
|
||||||
|
$vis = Get-FieldValue $entry @("visibility","Visibility")
|
||||||
|
$common = if ($null -ne $vis) { [bool]$vis } else { $true }
|
||||||
|
$roles = Get-FieldValue $entry @("roles")
|
||||||
|
}
|
||||||
|
|
||||||
|
$visParts = @()
|
||||||
|
$visParts += "$indent`t`t<xr:Common>$($common.ToString().ToLower())</xr:Common>"
|
||||||
|
if ($roles) {
|
||||||
|
# roles is PSCustomObject {Role.X: bool, ...}
|
||||||
|
foreach ($prop in $roles.PSObject.Properties) {
|
||||||
|
$rname = $prop.Name
|
||||||
|
if (-not $rname.StartsWith("Role.") -and -not ($rname -match '^[0-9a-fA-F]{8}-')) { $rname = "Role.$rname" }
|
||||||
|
$rval = ([bool]$prop.Value).ToString().ToLower()
|
||||||
|
$escName = [System.Security.SecurityElement]::Escape($rname)
|
||||||
|
$visParts += "$indent`t`t<xr:Value name=`"$escName`">$rval</xr:Value>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$visBlock = $visParts -join "`r`n"
|
||||||
|
$escForm = [System.Security.SecurityElement]::Escape($formRef)
|
||||||
|
return @"
|
||||||
|
$indent<Item>
|
||||||
|
$indent`t<Form>$escForm</Form>
|
||||||
|
$indent`t<Height>$height</Height>
|
||||||
|
$indent`t<Visibility>
|
||||||
|
$visBlock
|
||||||
|
$indent`t</Visibility>
|
||||||
|
$indent</Item>
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
function Do-SetHomePage($valArg) {
|
||||||
|
$layout = $valArg
|
||||||
|
if ($layout -is [string]) {
|
||||||
|
try { $layout = $layout | ConvertFrom-Json } catch {
|
||||||
|
Write-Error "set-home-page value must be valid JSON object"; exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (-not $layout) { Write-Error "set-home-page value is empty"; exit 1 }
|
||||||
|
|
||||||
|
$allowedTemplates = @("OneColumn","TwoColumnsEqualWidth","TwoColumnsVariableWidth")
|
||||||
|
$tmpl = Get-FieldValue $layout @("template","WorkingAreaTemplate")
|
||||||
|
if (-not $tmpl) { $tmpl = "TwoColumnsEqualWidth" }
|
||||||
|
if ($allowedTemplates -notcontains $tmpl) {
|
||||||
|
Write-Error "Unknown template '$tmpl'. Allowed: $($allowedTemplates -join ', ')"; exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$leftItems = Get-FieldValue $layout @("left","LeftColumn")
|
||||||
|
$rightItems = Get-FieldValue $layout @("right","RightColumn")
|
||||||
|
|
||||||
|
# Reject unknown keys
|
||||||
|
$known = @("template","WorkingAreaTemplate","left","LeftColumn","right","RightColumn")
|
||||||
|
foreach ($prop in $layout.PSObject.Properties) {
|
||||||
|
if ($known -notcontains $prop.Name) {
|
||||||
|
Write-Error "Unknown key '$($prop.Name)'. Allowed: template, left, right"; exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($tmpl -eq "OneColumn" -and $rightItems) {
|
||||||
|
Write-Error "Template 'OneColumn' cannot have items in 'right' column"; exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function Build-Column([string]$tag, $items) {
|
||||||
|
if (-not $items) { return "`t<$tag/>" }
|
||||||
|
if ($items -isnot [System.Array] -and $items -isnot [System.Collections.IList]) {
|
||||||
|
$items = @($items)
|
||||||
|
}
|
||||||
|
if ($items.Count -eq 0) { return "`t<$tag/>" }
|
||||||
|
$itemBlocks = @()
|
||||||
|
foreach ($it in $items) {
|
||||||
|
$itemBlocks += Build-HomePageItemXml $it "`t`t"
|
||||||
|
}
|
||||||
|
$body = $itemBlocks -join "`r`n"
|
||||||
|
return "`t<$tag>`r`n$body`r`n`t</$tag>"
|
||||||
|
}
|
||||||
|
|
||||||
|
$leftXml = Build-Column "LeftColumn" $leftItems
|
||||||
|
$rightXml = Build-Column "RightColumn" $rightItems
|
||||||
|
|
||||||
|
$hpXml = @"
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<HomePageWorkArea xmlns="http://v8.1c.ru/8.3/xcf/extrnprops" 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" version="2.17">
|
||||||
|
<WorkingAreaTemplate>$tmpl</WorkingAreaTemplate>
|
||||||
|
$leftXml
|
||||||
|
$rightXml
|
||||||
|
</HomePageWorkArea>
|
||||||
|
"@
|
||||||
|
|
||||||
|
$extDir = Join-Path $script:configDir "Ext"
|
||||||
|
if (-not (Test-Path $extDir)) { New-Item -ItemType Directory -Path $extDir -Force | Out-Null }
|
||||||
|
$hpPath = Join-Path $extDir "HomePageWorkArea.xml"
|
||||||
|
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||||
|
[System.IO.File]::WriteAllText($hpPath, $hpXml, $utf8Bom)
|
||||||
|
$script:modifyCount++
|
||||||
|
Info "Wrote home page layout: $hpPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 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 <DefaultRoles> 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 = "<xr:Item xsi:type=`"xr:MDObjectRef`">$roleName</xr:Item>"
|
||||||
|
$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" }
|
||||||
|
# Pass value through as-is (object or string); set-panels needs object form
|
||||||
|
$opValue = if ($null -ne $op.value) { $op.value } else { $Value }
|
||||||
|
$opValueStr = if ($opValue -is [string]) { $opValue } else { "$opValue" }
|
||||||
|
|
||||||
|
switch ($opName) {
|
||||||
|
"modify-property" { Do-ModifyProperty $opValueStr }
|
||||||
|
"add-childObject" { Do-AddChildObject $opValueStr }
|
||||||
|
"remove-childObject" { Do-RemoveChildObject $opValueStr }
|
||||||
|
"add-defaultRole" { Do-AddDefaultRole $opValueStr }
|
||||||
|
"remove-defaultRole" { Do-RemoveDefaultRole $opValueStr }
|
||||||
|
"set-defaultRoles" { Do-SetDefaultRoles $opValueStr }
|
||||||
|
"set-panels" { Do-SetPanels $opValue }
|
||||||
|
"set-home-page" { Do-SetHomePage $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
|
||||||
@@ -0,0 +1,985 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# cf-edit v1.7 — Edit 1C configuration root (Configuration.xml)
|
||||||
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import uuid as _uuid
|
||||||
|
from html import escape as html_escape
|
||||||
|
from lxml import etree
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
|
||||||
|
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
|
||||||
|
# present; reaction from .v8-project.json editingAllowedCheck (deny|warn|off,
|
||||||
|
# default deny). Never throws (except sys.exit on deny) — errors degrade to allow.
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def _sg_root_uuid(xml_path):
|
||||||
|
if not os.path.isfile(xml_path):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
mx = etree.parse(xml_path).getroot()
|
||||||
|
for child in mx:
|
||||||
|
if isinstance(child.tag, str) and child.get("uuid"):
|
||||||
|
return child.get("uuid")
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _sg_find_v8project(start_dir):
|
||||||
|
d = start_dir
|
||||||
|
for _ in range(20):
|
||||||
|
if not d:
|
||||||
|
break
|
||||||
|
pj = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pj):
|
||||||
|
return pj
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
break
|
||||||
|
d = parent
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _sg_get_edit_mode(cfg_dir):
|
||||||
|
try:
|
||||||
|
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(cfg_dir)
|
||||||
|
if not pj:
|
||||||
|
return "deny"
|
||||||
|
proj = json.loads(open(pj, encoding="utf-8-sig").read())
|
||||||
|
cfg_full = os.path.normcase(os.path.abspath(cfg_dir)).rstrip("\\/")
|
||||||
|
for db in proj.get("databases", []):
|
||||||
|
src = db.get("configSrc")
|
||||||
|
if src:
|
||||||
|
src_full = os.path.normcase(os.path.abspath(src)).rstrip("\\/")
|
||||||
|
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
|
||||||
|
if db.get("editingAllowedCheck"):
|
||||||
|
return db["editingAllowedCheck"]
|
||||||
|
if proj.get("editingAllowedCheck"):
|
||||||
|
return proj["editingAllowedCheck"]
|
||||||
|
return "deny"
|
||||||
|
except Exception:
|
||||||
|
return "deny"
|
||||||
|
|
||||||
|
|
||||||
|
def assert_edit_allowed(target_path, require):
|
||||||
|
try:
|
||||||
|
rp = os.path.abspath(target_path)
|
||||||
|
elem_uuid = _sg_root_uuid(rp)
|
||||||
|
cfg_dir = None
|
||||||
|
bin_path = None
|
||||||
|
d = rp if os.path.isdir(rp) else os.path.dirname(rp)
|
||||||
|
for _ in range(12):
|
||||||
|
if not d:
|
||||||
|
break
|
||||||
|
if not elem_uuid:
|
||||||
|
elem_uuid = _sg_root_uuid(d + ".xml")
|
||||||
|
if not cfg_dir:
|
||||||
|
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
|
||||||
|
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
|
||||||
|
cfg_dir = d
|
||||||
|
bin_path = cand
|
||||||
|
if elem_uuid and cfg_dir:
|
||||||
|
break
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
break
|
||||||
|
d = parent
|
||||||
|
if not elem_uuid and cfg_dir:
|
||||||
|
elem_uuid = _sg_root_uuid(os.path.join(cfg_dir, "Configuration.xml"))
|
||||||
|
if not bin_path or not os.path.exists(bin_path):
|
||||||
|
return
|
||||||
|
data = open(bin_path, "rb").read()
|
||||||
|
if len(data) <= 32:
|
||||||
|
return
|
||||||
|
if data[:3] == b"\xef\xbb\xbf":
|
||||||
|
data = data[3:]
|
||||||
|
text = data.decode("utf-8", "replace")
|
||||||
|
h = re.match(r"\{6,(\d+),(\d+),", text)
|
||||||
|
if not h:
|
||||||
|
return
|
||||||
|
g = int(h.group(1))
|
||||||
|
k = int(h.group(2))
|
||||||
|
if k == 0:
|
||||||
|
return
|
||||||
|
best = None
|
||||||
|
if elem_uuid:
|
||||||
|
for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
|
||||||
|
f1 = int(m.group(1))
|
||||||
|
if best is None or f1 < best:
|
||||||
|
best = f1
|
||||||
|
blocked = False
|
||||||
|
code = ""
|
||||||
|
reason = ""
|
||||||
|
if g == 1:
|
||||||
|
blocked = True
|
||||||
|
code = "capability-off"
|
||||||
|
reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)"
|
||||||
|
elif require == "removed":
|
||||||
|
if best is not None and best != 2:
|
||||||
|
blocked = True
|
||||||
|
code = "not-removed"
|
||||||
|
reason = "объект не снят с поддержки — удаление сломает обновления"
|
||||||
|
else:
|
||||||
|
if best is not None and best == 0:
|
||||||
|
blocked = True
|
||||||
|
code = "locked"
|
||||||
|
reason = "объект на замке — редактирование сломает обновления"
|
||||||
|
if not blocked:
|
||||||
|
return
|
||||||
|
mode = _sg_get_edit_mode(cfg_dir)
|
||||||
|
if mode == "off":
|
||||||
|
return
|
||||||
|
if mode == "warn":
|
||||||
|
sys.stderr.write(f"[support-guard] ПРЕДУПРЕЖДЕНИЕ: {reason}. Цель: {rp}\n")
|
||||||
|
return
|
||||||
|
head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
|
||||||
|
cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
|
||||||
|
off_note = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
|
||||||
|
if code == "capability-off":
|
||||||
|
state = f"Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «{rp}» редактировать нельзя."
|
||||||
|
fix = (
|
||||||
|
"Либо снять защиту явно (навык support-edit, два шага):\n"
|
||||||
|
f' 1. support-edit -Path "{cfg_dir}" -Capability on — включить возможность изменения (объекты пока остаются на замке);\n'
|
||||||
|
f' 2. support-edit -Path "{rp}" -Set editable — открыть этот объект для редактирования.\n'
|
||||||
|
" Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
|
||||||
|
)
|
||||||
|
elif code == "not-removed":
|
||||||
|
state = f"Состояние: объект «{rp}» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
|
||||||
|
fix = (
|
||||||
|
"Либо сначала снять объект с поддержки, затем удалять:\n"
|
||||||
|
f' support-edit -Path "{rp}" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно.'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
state = f"Состояние: объект «{rp}» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
|
||||||
|
fix = (
|
||||||
|
"Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):\n"
|
||||||
|
f' support-edit -Path "{rp}" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);\n'
|
||||||
|
f' support-edit -Path "{rp}" -Set off-support — снять с поддержки: обновления по объекту больше не приходят.'
|
||||||
|
)
|
||||||
|
sys.stderr.write(head + "\n" + state + "\n" + cfe + "\n" + fix + "\n" + off_note + "\n")
|
||||||
|
sys.exit(1)
|
||||||
|
except SystemExit:
|
||||||
|
raise
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
MD_NS = "http://v8.1c.ru/8.3/MDClasses"
|
||||||
|
XR_NS = "http://v8.1c.ru/8.3/xcf/readable"
|
||||||
|
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
V8_NS = "http://v8.1c.ru/8.1/data/core"
|
||||||
|
XS_NS = "http://www.w3.org/2001/XMLSchema"
|
||||||
|
|
||||||
|
# Canonical type order for ChildObjects (44 types)
|
||||||
|
TYPE_ORDER = [
|
||||||
|
"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 → on-disk directory name (plural)
|
||||||
|
TYPE_TO_DIR = {
|
||||||
|
"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",
|
||||||
|
}
|
||||||
|
|
||||||
|
ML_PROPS = ["Synonym", "BriefInformation", "DetailedInformation", "Copyright", "VendorInformationAddress", "ConfigurationInformationAddress"]
|
||||||
|
SCALAR_PROPS = ["Name", "Version", "Vendor", "Comment", "NamePrefix", "UpdateCatalogAddress"]
|
||||||
|
REF_PROPS = ["DefaultLanguage"]
|
||||||
|
|
||||||
|
|
||||||
|
def localname(el):
|
||||||
|
return etree.QName(el.tag).localname
|
||||||
|
|
||||||
|
|
||||||
|
def info(msg):
|
||||||
|
print(f"[INFO] {msg}")
|
||||||
|
|
||||||
|
|
||||||
|
def warn(msg):
|
||||||
|
print(f"[WARN] {msg}")
|
||||||
|
|
||||||
|
|
||||||
|
def get_child_indent(container):
|
||||||
|
if container.text and "\n" in container.text:
|
||||||
|
after_nl = container.text.rsplit("\n", 1)[-1]
|
||||||
|
if after_nl and not after_nl.strip():
|
||||||
|
return after_nl
|
||||||
|
for child in container:
|
||||||
|
if child.tail and "\n" in child.tail:
|
||||||
|
after_nl = child.tail.rsplit("\n", 1)[-1]
|
||||||
|
if after_nl and not after_nl.strip():
|
||||||
|
return after_nl
|
||||||
|
depth = 0
|
||||||
|
current = container
|
||||||
|
while current is not None:
|
||||||
|
depth += 1
|
||||||
|
current = current.getparent()
|
||||||
|
return "\t" * depth
|
||||||
|
|
||||||
|
|
||||||
|
def insert_before_closing(container, new_el, child_indent):
|
||||||
|
children = list(container)
|
||||||
|
if len(children) == 0:
|
||||||
|
parent_indent = child_indent[:-1] if len(child_indent) > 0 else ""
|
||||||
|
container.text = "\r\n" + child_indent
|
||||||
|
new_el.tail = "\r\n" + parent_indent
|
||||||
|
container.append(new_el)
|
||||||
|
else:
|
||||||
|
last = children[-1]
|
||||||
|
new_el.tail = last.tail
|
||||||
|
last.tail = "\r\n" + child_indent
|
||||||
|
container.append(new_el)
|
||||||
|
|
||||||
|
|
||||||
|
def insert_before_ref(container, new_el, ref_el, child_indent):
|
||||||
|
"""Insert new_el before ref_el inside container."""
|
||||||
|
idx = list(container).index(ref_el)
|
||||||
|
prev = ref_el.getprevious()
|
||||||
|
if prev is not None:
|
||||||
|
new_el.tail = prev.tail
|
||||||
|
prev.tail = "\r\n" + child_indent
|
||||||
|
else:
|
||||||
|
new_el.tail = container.text
|
||||||
|
container.text = "\r\n" + child_indent
|
||||||
|
container.insert(idx, new_el)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_with_indent(el):
|
||||||
|
parent = el.getparent()
|
||||||
|
prev = el.getprevious()
|
||||||
|
if prev is not None:
|
||||||
|
if el.tail:
|
||||||
|
prev.tail = el.tail
|
||||||
|
else:
|
||||||
|
if el.tail:
|
||||||
|
parent.text = el.tail
|
||||||
|
parent.remove(el)
|
||||||
|
|
||||||
|
|
||||||
|
def expand_self_closing(container, parent_indent):
|
||||||
|
if len(container) == 0 and not (container.text and container.text.strip()):
|
||||||
|
container.text = "\r\n" + parent_indent
|
||||||
|
|
||||||
|
|
||||||
|
def import_fragment(xml_string):
|
||||||
|
wrapper = (
|
||||||
|
f'<_W xmlns="{MD_NS}" xmlns:xsi="{XSI_NS}" xmlns:v8="{V8_NS}" '
|
||||||
|
f'xmlns:xr="{XR_NS}" xmlns:xs="{XS_NS}">{xml_string}</_W>'
|
||||||
|
)
|
||||||
|
frag = etree.fromstring(wrapper.encode("utf-8"))
|
||||||
|
return list(frag)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_batch_value(val):
|
||||||
|
items = []
|
||||||
|
for part in val.split(";;"):
|
||||||
|
trimmed = part.strip()
|
||||||
|
if trimmed:
|
||||||
|
items.append(trimmed)
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
|
def save_xml_bom(tree, path):
|
||||||
|
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
||||||
|
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
||||||
|
if not xml_bytes.endswith(b"\n"):
|
||||||
|
xml_bytes += b"\n"
|
||||||
|
with open(path, "wb") as f:
|
||||||
|
f.write(b"\xef\xbb\xbf")
|
||||||
|
f.write(xml_bytes)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
|
parser = argparse.ArgumentParser(description="Edit 1C configuration root (Configuration.xml)", allow_abbrev=False)
|
||||||
|
parser.add_argument("-ConfigPath", "-Path", required=True)
|
||||||
|
parser.add_argument("-DefinitionFile", default=None)
|
||||||
|
parser.add_argument("-Operation", default=None, choices=["modify-property", "add-childObject", "remove-childObject", "add-defaultRole", "remove-defaultRole", "set-defaultRoles", "set-panels", "set-home-page"])
|
||||||
|
parser.add_argument("-Value", default=None)
|
||||||
|
parser.add_argument("-NoValidate", action="store_true")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.DefinitionFile and args.Operation:
|
||||||
|
print("Cannot use both -DefinitionFile and -Operation", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if not args.DefinitionFile and not args.Operation:
|
||||||
|
print("Either -DefinitionFile or -Operation is required", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
config_path = args.ConfigPath
|
||||||
|
if not os.path.isabs(config_path):
|
||||||
|
config_path = os.path.join(os.getcwd(), config_path)
|
||||||
|
if os.path.isdir(config_path):
|
||||||
|
candidate = os.path.join(config_path, "Configuration.xml")
|
||||||
|
if os.path.isfile(candidate):
|
||||||
|
config_path = candidate
|
||||||
|
else:
|
||||||
|
print("No Configuration.xml in directory", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if not os.path.isfile(config_path):
|
||||||
|
print(f"File not found: {config_path}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
resolved_path = os.path.abspath(config_path)
|
||||||
|
config_dir = os.path.dirname(resolved_path)
|
||||||
|
|
||||||
|
assert_edit_allowed(resolved_path, "editable")
|
||||||
|
|
||||||
|
xml_parser = etree.XMLParser(remove_blank_text=False)
|
||||||
|
tree = etree.parse(resolved_path, xml_parser)
|
||||||
|
xml_root = tree.getroot()
|
||||||
|
|
||||||
|
add_count = 0
|
||||||
|
remove_count = 0
|
||||||
|
modify_count = 0
|
||||||
|
|
||||||
|
cfg_el = None
|
||||||
|
for child in xml_root:
|
||||||
|
if isinstance(child.tag, str) and localname(child) == "Configuration":
|
||||||
|
cfg_el = child
|
||||||
|
break
|
||||||
|
if cfg_el is None:
|
||||||
|
print("No <Configuration> element found", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
props_el = None
|
||||||
|
child_objs_el = None
|
||||||
|
for child in cfg_el:
|
||||||
|
if not isinstance(child.tag, str):
|
||||||
|
continue
|
||||||
|
if localname(child) == "Properties":
|
||||||
|
props_el = child
|
||||||
|
if localname(child) == "ChildObjects":
|
||||||
|
child_objs_el = child
|
||||||
|
|
||||||
|
obj_name = ""
|
||||||
|
if props_el is not None:
|
||||||
|
for child in props_el:
|
||||||
|
if isinstance(child.tag, str) and localname(child) == "Name":
|
||||||
|
obj_name = (child.text or "").strip()
|
||||||
|
break
|
||||||
|
info(f"Configuration: {obj_name}")
|
||||||
|
|
||||||
|
# --- Operations ---
|
||||||
|
def do_modify_property(batch_val):
|
||||||
|
nonlocal modify_count
|
||||||
|
items = parse_batch_value(batch_val)
|
||||||
|
for item in items:
|
||||||
|
eq_idx = item.find("=")
|
||||||
|
if eq_idx < 1:
|
||||||
|
print(f"Invalid property format '{item}', expected 'Key=Value'", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
prop_name = item[:eq_idx].strip()
|
||||||
|
prop_value = item[eq_idx + 1:].strip()
|
||||||
|
|
||||||
|
prop_el = None
|
||||||
|
for child in props_el:
|
||||||
|
if isinstance(child.tag, str) and localname(child) == prop_name:
|
||||||
|
prop_el = child
|
||||||
|
break
|
||||||
|
if prop_el is None:
|
||||||
|
print(f"Property '{prop_name}' not found in Properties", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if prop_name in ML_PROPS:
|
||||||
|
for ch in list(prop_el):
|
||||||
|
prop_el.remove(ch)
|
||||||
|
if not prop_value:
|
||||||
|
prop_el.text = None
|
||||||
|
else:
|
||||||
|
indent = get_child_indent(props_el)
|
||||||
|
item_el = etree.SubElement(prop_el, f"{{{V8_NS}}}item")
|
||||||
|
lang_el = etree.SubElement(item_el, f"{{{V8_NS}}}lang")
|
||||||
|
lang_el.text = "ru"
|
||||||
|
content_el = etree.SubElement(item_el, f"{{{V8_NS}}}content")
|
||||||
|
content_el.text = prop_value
|
||||||
|
prop_el.text = "\r\n" + indent + "\t"
|
||||||
|
item_el.text = "\r\n" + indent + "\t\t"
|
||||||
|
lang_el.tail = "\r\n" + indent + "\t\t"
|
||||||
|
content_el.tail = "\r\n" + indent + "\t"
|
||||||
|
item_el.tail = "\r\n" + indent
|
||||||
|
elif prop_name in SCALAR_PROPS or prop_name in REF_PROPS:
|
||||||
|
for ch in list(prop_el):
|
||||||
|
prop_el.remove(ch)
|
||||||
|
if not prop_value:
|
||||||
|
prop_el.text = None
|
||||||
|
else:
|
||||||
|
prop_el.text = prop_value
|
||||||
|
else:
|
||||||
|
for ch in list(prop_el):
|
||||||
|
prop_el.remove(ch)
|
||||||
|
prop_el.text = prop_value
|
||||||
|
|
||||||
|
modify_count += 1
|
||||||
|
info(f'Set {prop_name} = "{prop_value}"')
|
||||||
|
|
||||||
|
def do_add_child_object(batch_val):
|
||||||
|
nonlocal add_count
|
||||||
|
if child_objs_el is None:
|
||||||
|
print("No <ChildObjects> element found", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
items = parse_batch_value(batch_val)
|
||||||
|
cfg_indent = get_child_indent(cfg_el)
|
||||||
|
if len(child_objs_el) == 0 and not (child_objs_el.text and child_objs_el.text.strip()):
|
||||||
|
expand_self_closing(child_objs_el, cfg_indent)
|
||||||
|
child_indent = get_child_indent(child_objs_el)
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
dot_idx = item.find(".")
|
||||||
|
if dot_idx < 1:
|
||||||
|
print(f"Invalid format '{item}', expected 'Type.Name'", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
type_name = item[:dot_idx]
|
||||||
|
obj_name_val = item[dot_idx + 1:]
|
||||||
|
|
||||||
|
if type_name not in TYPE_ORDER:
|
||||||
|
print(f"Unknown type '{type_name}'", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
type_idx = TYPE_ORDER.index(type_name)
|
||||||
|
|
||||||
|
# Check that the referenced object actually exists on disk.
|
||||||
|
# cf-edit add-childObject is a low-level operation for rare scenarios
|
||||||
|
# (e.g. restoring a rolled-back Configuration.xml when object files are intact).
|
||||||
|
# For creating NEW objects, meta-compile/role-compile/subsystem-compile already
|
||||||
|
# auto-register in Configuration.xml — calling cf-edit add-childObject there is
|
||||||
|
# unnecessary and error-prone.
|
||||||
|
type_dir = TYPE_TO_DIR.get(type_name)
|
||||||
|
obj_file = os.path.join(config_dir, type_dir, f"{obj_name_val}.xml")
|
||||||
|
if not os.path.exists(obj_file):
|
||||||
|
hint_skill = {"Subsystem": "subsystem-compile", "Role": "role-compile"}.get(type_name, "meta-compile")
|
||||||
|
print(
|
||||||
|
f"Object file not found: {type_dir}/{obj_name_val}.xml\n"
|
||||||
|
f"cf-edit add-childObject only references objects that already exist on disk.\n"
|
||||||
|
f"To create a new {type_name}, use {hint_skill} (auto-registers in Configuration.xml):\n"
|
||||||
|
f' /{hint_skill} with {{"type":"{type_name}","name":"{obj_name_val}"}}',
|
||||||
|
file=sys.stderr
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Dedup
|
||||||
|
exists = False
|
||||||
|
for child in child_objs_el:
|
||||||
|
if isinstance(child.tag, str) and localname(child) == type_name and (child.text or "") == obj_name_val:
|
||||||
|
exists = True
|
||||||
|
break
|
||||||
|
if exists:
|
||||||
|
warn(f"Already exists: {type_name}.{obj_name_val}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Find insertion point
|
||||||
|
insert_before = None
|
||||||
|
for child in child_objs_el:
|
||||||
|
if not isinstance(child.tag, str):
|
||||||
|
continue
|
||||||
|
child_type_name = localname(child)
|
||||||
|
if child_type_name not in TYPE_ORDER:
|
||||||
|
continue
|
||||||
|
child_type_idx = TYPE_ORDER.index(child_type_name)
|
||||||
|
|
||||||
|
if child_type_name == type_name:
|
||||||
|
if (child.text or "") > obj_name_val and insert_before is None:
|
||||||
|
insert_before = child
|
||||||
|
elif child_type_idx > type_idx and insert_before is None:
|
||||||
|
insert_before = child
|
||||||
|
|
||||||
|
new_el = etree.Element(f"{{{MD_NS}}}{type_name}")
|
||||||
|
new_el.text = obj_name_val
|
||||||
|
|
||||||
|
if insert_before is not None:
|
||||||
|
insert_before_ref(child_objs_el, new_el, insert_before, child_indent)
|
||||||
|
else:
|
||||||
|
insert_before_closing(child_objs_el, new_el, child_indent)
|
||||||
|
|
||||||
|
add_count += 1
|
||||||
|
info(f"Added: {type_name}.{obj_name_val}")
|
||||||
|
|
||||||
|
def do_remove_child_object(batch_val):
|
||||||
|
nonlocal remove_count
|
||||||
|
if child_objs_el is None:
|
||||||
|
print("No <ChildObjects> element found", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
items = parse_batch_value(batch_val)
|
||||||
|
for item in items:
|
||||||
|
dot_idx = item.find(".")
|
||||||
|
if dot_idx < 1:
|
||||||
|
print(f"Invalid format '{item}', expected 'Type.Name'", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
type_name = item[:dot_idx]
|
||||||
|
obj_name_val = item[dot_idx + 1:]
|
||||||
|
|
||||||
|
found = False
|
||||||
|
for child in list(child_objs_el):
|
||||||
|
if isinstance(child.tag, str) and localname(child) == type_name and (child.text or "") == obj_name_val:
|
||||||
|
remove_with_indent(child)
|
||||||
|
remove_count += 1
|
||||||
|
info(f"Removed: {type_name}.{obj_name_val}")
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
if not found:
|
||||||
|
warn(f"Not found: {type_name}.{obj_name_val}")
|
||||||
|
|
||||||
|
def do_add_default_role(batch_val):
|
||||||
|
nonlocal add_count
|
||||||
|
items = parse_batch_value(batch_val)
|
||||||
|
|
||||||
|
roles_el = None
|
||||||
|
for child in props_el:
|
||||||
|
if isinstance(child.tag, str) and localname(child) == "DefaultRoles":
|
||||||
|
roles_el = child
|
||||||
|
break
|
||||||
|
if roles_el is None:
|
||||||
|
print("No <DefaultRoles> element found in Properties", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
props_indent = get_child_indent(props_el)
|
||||||
|
if len(roles_el) == 0 and not (roles_el.text and roles_el.text.strip()):
|
||||||
|
expand_self_closing(roles_el, props_indent)
|
||||||
|
role_indent = get_child_indent(roles_el)
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
role_name = item
|
||||||
|
if not role_name.startswith("Role."):
|
||||||
|
role_name = f"Role.{role_name}"
|
||||||
|
|
||||||
|
exists = False
|
||||||
|
for child in roles_el:
|
||||||
|
if isinstance(child.tag, str) and (child.text or "").strip() == role_name:
|
||||||
|
exists = True
|
||||||
|
break
|
||||||
|
if exists:
|
||||||
|
warn(f"DefaultRole already exists: {role_name}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
frag_xml = f'<xr:Item xsi:type="xr:MDObjectRef">{role_name}</xr:Item>'
|
||||||
|
nodes = import_fragment(frag_xml)
|
||||||
|
if nodes:
|
||||||
|
insert_before_closing(roles_el, nodes[0], role_indent)
|
||||||
|
add_count += 1
|
||||||
|
info(f"Added DefaultRole: {role_name}")
|
||||||
|
|
||||||
|
def do_remove_default_role(batch_val):
|
||||||
|
nonlocal remove_count
|
||||||
|
items = parse_batch_value(batch_val)
|
||||||
|
|
||||||
|
roles_el = None
|
||||||
|
for child in props_el:
|
||||||
|
if isinstance(child.tag, str) and localname(child) == "DefaultRoles":
|
||||||
|
roles_el = child
|
||||||
|
break
|
||||||
|
if roles_el is None:
|
||||||
|
print("No <DefaultRoles> element found", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
role_name = item
|
||||||
|
if not role_name.startswith("Role."):
|
||||||
|
role_name = f"Role.{role_name}"
|
||||||
|
|
||||||
|
found = False
|
||||||
|
for child in list(roles_el):
|
||||||
|
if isinstance(child.tag, str) and (child.text or "").strip() == role_name:
|
||||||
|
remove_with_indent(child)
|
||||||
|
remove_count += 1
|
||||||
|
info(f"Removed DefaultRole: {role_name}")
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
if not found:
|
||||||
|
warn(f"DefaultRole not found: {role_name}")
|
||||||
|
|
||||||
|
def do_set_default_roles(batch_val):
|
||||||
|
nonlocal modify_count
|
||||||
|
items = parse_batch_value(batch_val)
|
||||||
|
|
||||||
|
roles_el = None
|
||||||
|
for child in props_el:
|
||||||
|
if isinstance(child.tag, str) and localname(child) == "DefaultRoles":
|
||||||
|
roles_el = child
|
||||||
|
break
|
||||||
|
if roles_el is None:
|
||||||
|
print("No <DefaultRoles> element found", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Clear all existing children
|
||||||
|
for ch in list(roles_el):
|
||||||
|
roles_el.remove(ch)
|
||||||
|
roles_el.text = None
|
||||||
|
|
||||||
|
if not items:
|
||||||
|
modify_count += 1
|
||||||
|
info("Cleared DefaultRoles")
|
||||||
|
return
|
||||||
|
|
||||||
|
props_indent = get_child_indent(props_el)
|
||||||
|
role_indent = props_indent + "\t"
|
||||||
|
|
||||||
|
roles_el.text = "\r\n" + props_indent
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
role_name = item
|
||||||
|
if not role_name.startswith("Role."):
|
||||||
|
role_name = f"Role.{role_name}"
|
||||||
|
|
||||||
|
frag_xml = f'<xr:Item xsi:type="xr:MDObjectRef">{role_name}</xr:Item>'
|
||||||
|
nodes = import_fragment(frag_xml)
|
||||||
|
if nodes:
|
||||||
|
insert_before_closing(roles_el, nodes[0], role_indent)
|
||||||
|
|
||||||
|
modify_count += 1
|
||||||
|
info(f"Set DefaultRoles: {len(items)} roles")
|
||||||
|
|
||||||
|
# --- set-panels (writes Ext/ClientApplicationInterface.xml from scratch) ---
|
||||||
|
# Canonical English aliases — preferred form, used in docs and error messages.
|
||||||
|
PANEL_UUIDS = {
|
||||||
|
"sections": "b553047f-c9aa-4157-978d-448ecad24248",
|
||||||
|
"open": "cbab57f2-a0f3-4f0a-89ea-4cb19570ab75",
|
||||||
|
"favorites": "13322b22-3960-4d68-93a6-fe2dd7f28ca3",
|
||||||
|
"history": "c933ac92-92cd-459d-81cc-e0c8a83ced99",
|
||||||
|
"functions": "b2735bd3-d822-4430-ba59-c9e869693b24",
|
||||||
|
}
|
||||||
|
# Russian synonyms — silently accepted (cf-info displays Russian names;
|
||||||
|
# users may copy them straight into cf-edit value).
|
||||||
|
PANEL_SYNONYMS = {
|
||||||
|
"разделов": "sections", "разделы": "sections",
|
||||||
|
"открытых": "open", "открытые": "open",
|
||||||
|
"избранного": "favorites","избранное": "favorites",
|
||||||
|
"истории": "history", "история": "history",
|
||||||
|
"функций": "functions", "функции": "functions",
|
||||||
|
}
|
||||||
|
|
||||||
|
def build_panel_entry_xml(entry, indent):
|
||||||
|
if isinstance(entry, str):
|
||||||
|
key = entry.lower()
|
||||||
|
key = PANEL_SYNONYMS.get(key, key)
|
||||||
|
if key not in PANEL_UUIDS:
|
||||||
|
allowed = ", ".join(sorted(PANEL_UUIDS.keys()))
|
||||||
|
print(f"Unknown panel alias '{entry}'. Allowed: {allowed}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
inst = str(_uuid.uuid4())
|
||||||
|
return f'{indent}<panel id="{inst}">\r\n{indent}\t<uuid>{PANEL_UUIDS[key]}</uuid>\r\n{indent}</panel>'
|
||||||
|
if isinstance(entry, dict) and "group" in entry:
|
||||||
|
children = entry["group"]
|
||||||
|
if not children:
|
||||||
|
print("group must contain at least one entry", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
gid = str(_uuid.uuid4())
|
||||||
|
inner = ""
|
||||||
|
for child in children:
|
||||||
|
child_xml = build_panel_entry_xml(child, indent + "\t\t")
|
||||||
|
inner += f"{indent}\t<group>\r\n{child_xml}\r\n{indent}\t</group>\r\n"
|
||||||
|
return f'{indent}<group id="{gid}">\r\n{inner}{indent}</group>'
|
||||||
|
print(f"Panel entry must be string alias or {{group:[...]}}, got: {entry!r}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def do_set_panels(value):
|
||||||
|
nonlocal modify_count
|
||||||
|
layout = value
|
||||||
|
if isinstance(layout, str):
|
||||||
|
try:
|
||||||
|
layout = json.loads(layout)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print(f"set-panels value must be valid JSON object", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if not isinstance(layout, dict) or not layout:
|
||||||
|
print("set-panels value must be non-empty object", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
sides = ("top", "left", "right", "bottom")
|
||||||
|
# Reject unknown side keys
|
||||||
|
for k in layout.keys():
|
||||||
|
if k not in sides:
|
||||||
|
print(f"Unknown side '{k}'. Allowed: {', '.join(sides)}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
body_parts = []
|
||||||
|
for side in sides:
|
||||||
|
entries = layout.get(side)
|
||||||
|
if entries is None:
|
||||||
|
continue
|
||||||
|
if not isinstance(entries, list):
|
||||||
|
entries = [entries]
|
||||||
|
for entry in entries:
|
||||||
|
entry_xml = build_panel_entry_xml(entry, "\t\t")
|
||||||
|
body_parts.append(f"\t<{side}>\r\n{entry_xml}\r\n\t</{side}>")
|
||||||
|
body = "\r\n".join(body_parts)
|
||||||
|
body_block = body + "\r\n" if body else ""
|
||||||
|
declarations = (
|
||||||
|
'\t<panelDef id="b553047f-c9aa-4157-978d-448ecad24248"/>\r\n'
|
||||||
|
'\t<panelDef id="13322b22-3960-4d68-93a6-fe2dd7f28ca3"/>\r\n'
|
||||||
|
'\t<panelDef id="c933ac92-92cd-459d-81cc-e0c8a83ced99"/>\r\n'
|
||||||
|
'\t<panelDef id="cbab57f2-a0f3-4f0a-89ea-4cb19570ab75"/>\r\n'
|
||||||
|
'\t<panelDef id="b2735bd3-d822-4430-ba59-c9e869693b24"/>'
|
||||||
|
)
|
||||||
|
cai_xml = (
|
||||||
|
'<?xml version="1.0" encoding="UTF-8"?>\r\n'
|
||||||
|
'<ClientApplicationInterface xmlns="http://v8.1c.ru/8.2/managed-application/core" '
|
||||||
|
'xmlns:xs="http://www.w3.org/2001/XMLSchema" '
|
||||||
|
'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" '
|
||||||
|
'xsi:type="InterfaceLayouter">\r\n'
|
||||||
|
f'{body_block}{declarations}\r\n'
|
||||||
|
'</ClientApplicationInterface>'
|
||||||
|
)
|
||||||
|
ext_dir = os.path.join(config_dir, "Ext")
|
||||||
|
os.makedirs(ext_dir, exist_ok=True)
|
||||||
|
cai_path = os.path.join(ext_dir, "ClientApplicationInterface.xml")
|
||||||
|
with open(cai_path, "w", encoding="utf-8-sig", newline="") as fh:
|
||||||
|
fh.write(cai_xml)
|
||||||
|
modify_count += 1
|
||||||
|
info(f"Wrote panel layout: {cai_path}")
|
||||||
|
|
||||||
|
# --- set-home-page (writes Ext/HomePageWorkArea.xml from scratch) ---
|
||||||
|
RU_TYPE_MAP = {
|
||||||
|
"справочник": "Catalog", "документ": "Document", "перечисление": "Enum",
|
||||||
|
"отчёт": "Report", "отчет": "Report", "обработка": "DataProcessor",
|
||||||
|
"общаяформа": "CommonForm", "журналдокументов": "DocumentJournal",
|
||||||
|
"планвидовхарактеристик": "ChartOfCharacteristicTypes",
|
||||||
|
"плансчетов": "ChartOfAccounts",
|
||||||
|
"планвидоврасчета": "ChartOfCalculationTypes",
|
||||||
|
"планвидоврасчёта": "ChartOfCalculationTypes",
|
||||||
|
"регистрсведений": "InformationRegister",
|
||||||
|
"регистрнакопления": "AccumulationRegister",
|
||||||
|
"регистрбухгалтерии": "AccountingRegister",
|
||||||
|
"регистррасчета": "CalculationRegister",
|
||||||
|
"регистррасчёта": "CalculationRegister",
|
||||||
|
"бизнеспроцесс": "BusinessProcess",
|
||||||
|
"задача": "Task", "планобмена": "ExchangePlan",
|
||||||
|
"хранилищенастроек": "SettingsStorage",
|
||||||
|
}
|
||||||
|
DIR_TO_TYPE = {v.lower(): k for k, v in TYPE_TO_DIR.items()}
|
||||||
|
UUID_RE = __import__("re").compile(r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")
|
||||||
|
|
||||||
|
def normalize_form_ref(s):
|
||||||
|
s = (s or "").strip()
|
||||||
|
if not s:
|
||||||
|
return s
|
||||||
|
if UUID_RE.match(s):
|
||||||
|
return s
|
||||||
|
if "/" in s or "\\" in s:
|
||||||
|
parts = [p for p in s.replace("\\", "/").split("/") if p and p.lower() != "ext"]
|
||||||
|
if parts and parts[-1].lower() == "form.xml":
|
||||||
|
parts = parts[:-1]
|
||||||
|
if len(parts) >= 2:
|
||||||
|
type_dir = parts[0]
|
||||||
|
type_singular = DIR_TO_TYPE.get(type_dir.lower())
|
||||||
|
if type_singular:
|
||||||
|
if type_singular == "CommonForm" and len(parts) >= 2:
|
||||||
|
return f"CommonForm.{parts[1]}"
|
||||||
|
if len(parts) >= 4 and parts[2].lower() == "forms":
|
||||||
|
return f"{type_singular}.{parts[1]}.Form.{parts[3]}"
|
||||||
|
return s
|
||||||
|
segs = s.split(".")
|
||||||
|
if segs:
|
||||||
|
head = segs[0].lower()
|
||||||
|
if head in RU_TYPE_MAP:
|
||||||
|
segs[0] = RU_TYPE_MAP[head]
|
||||||
|
for i in range(1, len(segs)):
|
||||||
|
if segs[i] == "Форма":
|
||||||
|
segs[i] = "Form"
|
||||||
|
if len(segs) == 3 and segs[0] in TYPE_ORDER and segs[0] != "CommonForm":
|
||||||
|
segs = [segs[0], segs[1], "Form", segs[2]]
|
||||||
|
return ".".join(segs)
|
||||||
|
|
||||||
|
def get_field(obj, keys):
|
||||||
|
for k in keys:
|
||||||
|
if isinstance(obj, dict) and k in obj:
|
||||||
|
return obj[k]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def build_home_page_item_xml(entry, indent):
|
||||||
|
if isinstance(entry, str):
|
||||||
|
form_ref = normalize_form_ref(entry)
|
||||||
|
height = 10
|
||||||
|
common = True
|
||||||
|
roles = None
|
||||||
|
elif isinstance(entry, dict):
|
||||||
|
form_raw = get_field(entry, ["form", "Form"])
|
||||||
|
if not form_raw:
|
||||||
|
print(f"Home page item: 'form' is required, got: {entry!r}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
form_ref = normalize_form_ref(str(form_raw))
|
||||||
|
h = get_field(entry, ["height", "Height"])
|
||||||
|
height = int(h) if h is not None else 10
|
||||||
|
vis = get_field(entry, ["visibility", "Visibility"])
|
||||||
|
common = bool(vis) if vis is not None else True
|
||||||
|
roles = get_field(entry, ["roles"])
|
||||||
|
else:
|
||||||
|
print(f"Home page item must be string or object, got: {entry!r}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
vis_parts = [f"{indent}\t\t<xr:Common>{str(common).lower()}</xr:Common>"]
|
||||||
|
if roles and isinstance(roles, dict):
|
||||||
|
for rname, rval in roles.items():
|
||||||
|
if not rname.startswith("Role.") and not UUID_RE.match(rname):
|
||||||
|
rname = f"Role.{rname}"
|
||||||
|
rval_s = str(bool(rval)).lower()
|
||||||
|
vis_parts.append(f'{indent}\t\t<xr:Value name="{html_escape(rname, quote=True)}">{rval_s}</xr:Value>')
|
||||||
|
vis_block = "\r\n".join(vis_parts)
|
||||||
|
esc_form = html_escape(form_ref, quote=True)
|
||||||
|
return (
|
||||||
|
f"{indent}<Item>\r\n"
|
||||||
|
f"{indent}\t<Form>{esc_form}</Form>\r\n"
|
||||||
|
f"{indent}\t<Height>{height}</Height>\r\n"
|
||||||
|
f"{indent}\t<Visibility>\r\n"
|
||||||
|
f"{vis_block}\r\n"
|
||||||
|
f"{indent}\t</Visibility>\r\n"
|
||||||
|
f"{indent}</Item>"
|
||||||
|
)
|
||||||
|
|
||||||
|
def do_set_home_page(value):
|
||||||
|
nonlocal modify_count
|
||||||
|
layout = value
|
||||||
|
if isinstance(layout, str):
|
||||||
|
try:
|
||||||
|
layout = json.loads(layout)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print("set-home-page value must be valid JSON object", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if not isinstance(layout, dict) or not layout:
|
||||||
|
print("set-home-page value must be non-empty object", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
allowed_templates = ("OneColumn", "TwoColumnsEqualWidth", "TwoColumnsVariableWidth")
|
||||||
|
tmpl = get_field(layout, ["template", "WorkingAreaTemplate"]) or "TwoColumnsEqualWidth"
|
||||||
|
if tmpl not in allowed_templates:
|
||||||
|
print(f"Unknown template '{tmpl}'. Allowed: {', '.join(allowed_templates)}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
left_items = get_field(layout, ["left", "LeftColumn"])
|
||||||
|
right_items = get_field(layout, ["right", "RightColumn"])
|
||||||
|
|
||||||
|
known = {"template", "WorkingAreaTemplate", "left", "LeftColumn", "right", "RightColumn"}
|
||||||
|
for k in layout.keys():
|
||||||
|
if k not in known:
|
||||||
|
print(f"Unknown key '{k}'. Allowed: template, left, right", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if tmpl == "OneColumn" and right_items:
|
||||||
|
print("Template 'OneColumn' cannot have items in 'right' column", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def build_column(tag, items):
|
||||||
|
if not items:
|
||||||
|
return f"\t<{tag}/>"
|
||||||
|
if not isinstance(items, list):
|
||||||
|
items = [items]
|
||||||
|
if not items:
|
||||||
|
return f"\t<{tag}/>"
|
||||||
|
blocks = [build_home_page_item_xml(it, "\t\t") for it in items]
|
||||||
|
body = "\r\n".join(blocks)
|
||||||
|
return f"\t<{tag}>\r\n{body}\r\n\t</{tag}>"
|
||||||
|
|
||||||
|
left_xml = build_column("LeftColumn", left_items)
|
||||||
|
right_xml = build_column("RightColumn", right_items)
|
||||||
|
|
||||||
|
hp_xml = (
|
||||||
|
'<?xml version="1.0" encoding="UTF-8"?>\r\n'
|
||||||
|
'<HomePageWorkArea xmlns="http://v8.1c.ru/8.3/xcf/extrnprops" '
|
||||||
|
'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" version="2.17">\r\n'
|
||||||
|
f'\t<WorkingAreaTemplate>{tmpl}</WorkingAreaTemplate>\r\n'
|
||||||
|
f'{left_xml}\r\n'
|
||||||
|
f'{right_xml}\r\n'
|
||||||
|
'</HomePageWorkArea>'
|
||||||
|
)
|
||||||
|
|
||||||
|
ext_dir = os.path.join(config_dir, "Ext")
|
||||||
|
os.makedirs(ext_dir, exist_ok=True)
|
||||||
|
hp_path = os.path.join(ext_dir, "HomePageWorkArea.xml")
|
||||||
|
with open(hp_path, "w", encoding="utf-8-sig", newline="") as fh:
|
||||||
|
fh.write(hp_xml)
|
||||||
|
modify_count += 1
|
||||||
|
info(f"Wrote home page layout: {hp_path}")
|
||||||
|
|
||||||
|
# --- Execute operations ---
|
||||||
|
operations = []
|
||||||
|
if args.DefinitionFile:
|
||||||
|
def_file = args.DefinitionFile
|
||||||
|
if not os.path.isabs(def_file):
|
||||||
|
def_file = os.path.join(os.getcwd(), def_file)
|
||||||
|
with open(def_file, "r", encoding="utf-8-sig") as fh:
|
||||||
|
ops = json.loads(fh.read())
|
||||||
|
if isinstance(ops, list):
|
||||||
|
operations = ops
|
||||||
|
else:
|
||||||
|
operations = [ops]
|
||||||
|
else:
|
||||||
|
operations = [{"operation": args.Operation, "value": args.Value or ""}]
|
||||||
|
|
||||||
|
for op in operations:
|
||||||
|
op_name = op.get("operation", args.Operation or "")
|
||||||
|
op_value = op.get("value", args.Value or "")
|
||||||
|
|
||||||
|
if op_name == "modify-property":
|
||||||
|
do_modify_property(op_value if isinstance(op_value, str) else str(op_value))
|
||||||
|
elif op_name == "add-childObject":
|
||||||
|
do_add_child_object(op_value if isinstance(op_value, str) else str(op_value))
|
||||||
|
elif op_name == "remove-childObject":
|
||||||
|
do_remove_child_object(op_value if isinstance(op_value, str) else str(op_value))
|
||||||
|
elif op_name == "add-defaultRole":
|
||||||
|
do_add_default_role(op_value if isinstance(op_value, str) else str(op_value))
|
||||||
|
elif op_name == "remove-defaultRole":
|
||||||
|
do_remove_default_role(op_value if isinstance(op_value, str) else str(op_value))
|
||||||
|
elif op_name == "set-defaultRoles":
|
||||||
|
do_set_default_roles(op_value if isinstance(op_value, str) else str(op_value))
|
||||||
|
elif op_name == "set-panels":
|
||||||
|
do_set_panels(op_value)
|
||||||
|
elif op_name == "set-home-page":
|
||||||
|
do_set_home_page(op_value)
|
||||||
|
else:
|
||||||
|
print(f"Unknown operation: {op_name}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# --- Save ---
|
||||||
|
save_xml_bom(tree, resolved_path)
|
||||||
|
info(f"Saved: {resolved_path}")
|
||||||
|
|
||||||
|
# --- Auto-validate ---
|
||||||
|
if not args.NoValidate:
|
||||||
|
validate_script = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "..", "cf-validate", "scripts", "cf-validate.py"))
|
||||||
|
if os.path.isfile(validate_script):
|
||||||
|
print()
|
||||||
|
print("--- Running cf-validate ---")
|
||||||
|
subprocess.run([sys.executable, validate_script, "-ConfigPath", resolved_path])
|
||||||
|
|
||||||
|
# --- Summary ---
|
||||||
|
print()
|
||||||
|
print("=== cf-edit summary ===")
|
||||||
|
print(f" Configuration: {obj_name}")
|
||||||
|
print(f" Added: {add_count}")
|
||||||
|
print(f" Removed: {remove_count}")
|
||||||
|
print(f" Modified: {modify_count}")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: cf-info
|
name: cf-info
|
||||||
description: Анализ структуры конфигурации 1С — свойства, состав, счётчики объектов. Используй для обзора конфигурации — какие объекты есть, сколько их, какие настройки
|
description: Анализ структуры конфигурации 1С — свойства, состав, счётчики объектов. Используй для обзора конфигурации — какие объекты есть, сколько их, какие настройки
|
||||||
argument-hint: <ConfigPath> [-Mode overview|brief|full]
|
argument-hint: <ConfigPath> [-Mode overview|brief|full] [-Section home-page]
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash
|
- Bash
|
||||||
- Read
|
- Read
|
||||||
@@ -18,11 +18,12 @@ allowed-tools:
|
|||||||
|----------|----------|
|
|----------|----------|
|
||||||
| `ConfigPath` | Путь к Configuration.xml или каталогу выгрузки |
|
| `ConfigPath` | Путь к Configuration.xml или каталогу выгрузки |
|
||||||
| `Mode` | Режим: `overview` (default), `brief`, `full` |
|
| `Mode` | Режим: `overview` (default), `brief`, `full` |
|
||||||
|
| `Section` | Drill-down по разделу (alias: `Name`). Сейчас: `home-page` |
|
||||||
| `Limit` / `Offset` | Пагинация (по умолчанию 150 строк) |
|
| `Limit` / `Offset` | Пагинация (по умолчанию 150 строк) |
|
||||||
| `OutFile` | Записать результат в файл (UTF-8 BOM) |
|
| `OutFile` | Записать результат в файл (UTF-8 BOM) |
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File .claude/skills/cf-info/scripts/cf-info.ps1 -ConfigPath "<путь>"
|
python ".opencode/skills/cf-info/scripts/cf-info.py" -ConfigPath "<путь>"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Три режима
|
## Три режима
|
||||||
@@ -37,14 +38,17 @@ powershell.exe -NoProfile -File .claude/skills/cf-info/scripts/cf-info.ps1 -Conf
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Обзор пустой конфигурации
|
# Обзор пустой конфигурации
|
||||||
... -ConfigPath upload/cfempty
|
... -ConfigPath src
|
||||||
|
|
||||||
# Краткая сводка реальной конфигурации
|
# Краткая сводка реальной конфигурации
|
||||||
... -ConfigPath upload/acc_8.3.24 -Mode brief
|
... -ConfigPath src -Mode brief
|
||||||
|
|
||||||
# Полная информация
|
# Полная информация
|
||||||
... -ConfigPath upload/acc_8.3.24 -Mode full
|
... -ConfigPath src -Mode full
|
||||||
|
|
||||||
# С пагинацией
|
# С пагинацией
|
||||||
... -ConfigPath upload/acc_8.3.24 -Mode full -Limit 50 -Offset 100
|
... -ConfigPath src -Mode full -Limit 50 -Offset 100
|
||||||
|
|
||||||
|
# Drill-down: только начальная страница (раскладка форм с ролями)
|
||||||
|
... -ConfigPath src -Section home-page
|
||||||
```
|
```
|
||||||
+272
-5
@@ -1,9 +1,12 @@
|
|||||||
# cf-info v1.0 — Compact summary of 1C configuration root
|
# cf-info v1.3 — Compact summary of 1C configuration root
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory=$true)][string]$ConfigPath,
|
[Parameter(Mandatory=$true)][Alias('Path')][string]$ConfigPath,
|
||||||
[ValidateSet("overview","brief","full")]
|
[ValidateSet("overview","brief","full")]
|
||||||
[string]$Mode = "overview",
|
[string]$Mode = "overview",
|
||||||
|
[Alias('Name')]
|
||||||
|
[ValidateSet("home-page")]
|
||||||
|
[string]$Section,
|
||||||
[int]$Limit = 150,
|
[int]$Limit = 150,
|
||||||
[int]$Offset = 0,
|
[int]$Offset = 0,
|
||||||
[string]$OutFile
|
[string]$OutFile
|
||||||
@@ -118,6 +121,187 @@ $typeRuNames = @{
|
|||||||
"Task"="Задачи"; "IntegrationService"="Сервисы интеграции"
|
"Task"="Задачи"; "IntegrationService"="Сервисы интеграции"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Read panel layout (Ext/ClientApplicationInterface.xml) ---
|
||||||
|
$script:panelNames = @{
|
||||||
|
"cbab57f2-a0f3-4f0a-89ea-4cb19570ab75" = "Открытых"
|
||||||
|
"b553047f-c9aa-4157-978d-448ecad24248" = "Разделов"
|
||||||
|
"13322b22-3960-4d68-93a6-fe2dd7f28ca3" = "Избранного"
|
||||||
|
"c933ac92-92cd-459d-81cc-e0c8a83ced99" = "История"
|
||||||
|
"b2735bd3-d822-4430-ba59-c9e869693b24" = "Функций"
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-PanelsLayout {
|
||||||
|
$configDir = [System.IO.Path]::GetDirectoryName($ConfigPath)
|
||||||
|
$caiPath = Join-Path (Join-Path $configDir "Ext") "ClientApplicationInterface.xml"
|
||||||
|
if (-not (Test-Path $caiPath)) { return $null }
|
||||||
|
try { [xml]$caiDoc = Get-Content -Path $caiPath -Encoding UTF8 } catch { return $null }
|
||||||
|
if (-not $caiDoc.DocumentElement) { return $null }
|
||||||
|
$caiNs = New-Object System.Xml.XmlNamespaceManager($caiDoc.NameTable)
|
||||||
|
$caiNs.AddNamespace("ca", "http://v8.1c.ru/8.2/managed-application/core")
|
||||||
|
$layout = [ordered]@{ top=@(); left=@(); right=@(); bottom=@(); declared=@() }
|
||||||
|
foreach ($side in @("top","left","right","bottom")) {
|
||||||
|
foreach ($sideEl in $caiDoc.DocumentElement.SelectNodes("ca:$side", $caiNs)) {
|
||||||
|
$slot = @()
|
||||||
|
foreach ($u in $sideEl.SelectNodes(".//ca:panel/ca:uuid", $caiNs)) {
|
||||||
|
$key = $u.InnerText.Trim()
|
||||||
|
$nm = if ($script:panelNames.Contains($key)) { $script:panelNames[$key] } else { "?$key" }
|
||||||
|
$slot += $nm
|
||||||
|
}
|
||||||
|
if ($slot.Count -gt 0) { $layout[$side] += ,$slot }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($pd in $caiDoc.DocumentElement.SelectNodes("ca:panelDef", $caiNs)) {
|
||||||
|
$key = $pd.GetAttribute("id")
|
||||||
|
$nm = if ($script:panelNames.Contains($key)) { $script:panelNames[$key] } else { "?$key" }
|
||||||
|
$layout.declared += $nm
|
||||||
|
}
|
||||||
|
return $layout
|
||||||
|
}
|
||||||
|
|
||||||
|
function Format-LayoutSlots($slots) {
|
||||||
|
# slots is array of arrays (each inner array = one side-tag's panels, may be 1+)
|
||||||
|
# Single inner array, single panel -> just name
|
||||||
|
# Single inner array, multiple panels -> "Стек(a, b)"
|
||||||
|
# Multiple inner arrays -> separate entries joined by " | "
|
||||||
|
if (-not $slots -or $slots.Count -eq 0) { return "" }
|
||||||
|
$parts = @()
|
||||||
|
foreach ($slot in $slots) {
|
||||||
|
if ($slot.Count -eq 1) { $parts += $slot[0] }
|
||||||
|
else { $parts += ("Стек(" + ($slot -join ", ") + ")") }
|
||||||
|
}
|
||||||
|
return ($parts -join " | ")
|
||||||
|
}
|
||||||
|
|
||||||
|
$script:panelLayout = Get-PanelsLayout
|
||||||
|
|
||||||
|
# --- Read home page layout (Ext/HomePageWorkArea.xml) ---
|
||||||
|
function Get-HomePageLayout {
|
||||||
|
$configDir = [System.IO.Path]::GetDirectoryName($ConfigPath)
|
||||||
|
$hpPath = Join-Path (Join-Path $configDir "Ext") "HomePageWorkArea.xml"
|
||||||
|
if (-not (Test-Path $hpPath)) { return $null }
|
||||||
|
try { [xml]$hpDoc = Get-Content -Path $hpPath -Encoding UTF8 } catch { return $null }
|
||||||
|
if (-not $hpDoc.DocumentElement) { return $null }
|
||||||
|
$hpNs = New-Object System.Xml.XmlNamespaceManager($hpDoc.NameTable)
|
||||||
|
$hpNs.AddNamespace("hp", "http://v8.1c.ru/8.3/xcf/extrnprops")
|
||||||
|
$hpNs.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable")
|
||||||
|
$result = [ordered]@{ template = ""; left = @(); right = @() }
|
||||||
|
$tmplNode = $hpDoc.DocumentElement.SelectSingleNode("hp:WorkingAreaTemplate", $hpNs)
|
||||||
|
if ($tmplNode) { $result.template = $tmplNode.InnerText.Trim() }
|
||||||
|
foreach ($colName in @("LeftColumn","RightColumn")) {
|
||||||
|
$colNode = $hpDoc.DocumentElement.SelectSingleNode("hp:$colName", $hpNs)
|
||||||
|
if (-not $colNode) { continue }
|
||||||
|
$items = @()
|
||||||
|
foreach ($item in $colNode.SelectNodes("hp:Item", $hpNs)) {
|
||||||
|
$f = $item.SelectSingleNode("hp:Form", $hpNs)
|
||||||
|
$h = $item.SelectSingleNode("hp:Height", $hpNs)
|
||||||
|
$visNode = $item.SelectSingleNode("hp:Visibility", $hpNs)
|
||||||
|
$common = $true
|
||||||
|
$roles = @()
|
||||||
|
if ($visNode) {
|
||||||
|
$cn = $visNode.SelectSingleNode("xr:Common", $hpNs)
|
||||||
|
if ($cn) { $common = ($cn.InnerText.Trim() -eq "true") }
|
||||||
|
foreach ($v in $visNode.SelectNodes("xr:Value", $hpNs)) {
|
||||||
|
$roles += @{ name = $v.GetAttribute("name"); value = ($v.InnerText.Trim() -eq "true") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$items += [ordered]@{
|
||||||
|
form = if ($f) { $f.InnerText.Trim() } else { "" }
|
||||||
|
height = if ($h) { [int]$h.InnerText.Trim() } else { 10 }
|
||||||
|
common = $common
|
||||||
|
roles = $roles
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($colName -eq "LeftColumn") { $result.left = $items } else { $result.right = $items }
|
||||||
|
}
|
||||||
|
return $result
|
||||||
|
}
|
||||||
|
|
||||||
|
$script:homePage = Get-HomePageLayout
|
||||||
|
|
||||||
|
# --- Support state (Ext/ParentConfigurations.bin) ---
|
||||||
|
# Decodes the 1C support-state file. See docs/1c-support-state-spec.md.
|
||||||
|
# Returns $null on absent/error; else hashtable: State='absent'|'removed'|'parsed',
|
||||||
|
# G (0=editing on, 1=off), K (vendor configs), Vendors @(@{Vendor;Name;Version}),
|
||||||
|
# Counts @(locked, editable, removed) by f1 — record tally (K>1 counts each
|
||||||
|
# vendor block separately); only computed when G=0.
|
||||||
|
function Read-SupportState([string]$binPath) {
|
||||||
|
try {
|
||||||
|
if (-not (Test-Path $binPath)) { return @{ State = 'absent' } }
|
||||||
|
$bytes = [System.IO.File]::ReadAllBytes($binPath)
|
||||||
|
if ($bytes.Length -le 32) { return @{ State = 'removed' } }
|
||||||
|
$startIdx = 0
|
||||||
|
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $startIdx = 3 }
|
||||||
|
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $startIdx, $bytes.Length - $startIdx)
|
||||||
|
$h = [regex]::Match($text, '^\{6,(\d+),(\d+),')
|
||||||
|
if (-not $h.Success) { return $null }
|
||||||
|
$G = [int]$h.Groups[1].Value
|
||||||
|
$K = [int]$h.Groups[2].Value
|
||||||
|
if ($K -eq 0) { return @{ State = 'removed' } }
|
||||||
|
# Vendor descriptors: ...,"ver","vendor","name",count,
|
||||||
|
$vendors = @()
|
||||||
|
$vRe = [regex]'"((?:[^"]|"")*)","((?:[^"]|"")*)","((?:[^"]|"")*)",\d+,'
|
||||||
|
foreach ($m in $vRe.Matches($text)) {
|
||||||
|
$vendors += @{
|
||||||
|
Version = ($m.Groups[1].Value -replace '""','"')
|
||||||
|
Vendor = ($m.Groups[2].Value -replace '""','"')
|
||||||
|
Name = ($m.Groups[3].Value -replace '""','"')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# Per-object counts only matter when editing is enabled (G=0); when G=1 the
|
||||||
|
# whole config is read-only and stored f1 values are the inactive default.
|
||||||
|
$counts = $null
|
||||||
|
if ($G -eq 0) {
|
||||||
|
$counts = @(0, 0, 0)
|
||||||
|
# Object records: f1,0,uuidLocal[,uuidVendor] — flags precede the uuid.
|
||||||
|
$rRe = [regex]'([0-2]),0,[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
|
||||||
|
foreach ($m in $rRe.Matches($text)) {
|
||||||
|
$counts[[int]$m.Groups[1].Value]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return @{ State = 'parsed'; G = $G; K = $K; Vendors = $vendors; Counts = $counts }
|
||||||
|
} catch { return $null }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-SupportLines {
|
||||||
|
$configDir = [System.IO.Path]::GetDirectoryName($ConfigPath)
|
||||||
|
$binPath = Join-Path (Join-Path $configDir "Ext") "ParentConfigurations.bin"
|
||||||
|
$st = Read-SupportState $binPath
|
||||||
|
$out = @()
|
||||||
|
if (-not $st -or $st.State -eq 'absent') {
|
||||||
|
if ($cfgExtPurpose) { $out += "Поддержка: расширение (CFE), правки свободны" }
|
||||||
|
else { $out += "Поддержка: не на поддержке (своя конфигурация)" }
|
||||||
|
return $out
|
||||||
|
}
|
||||||
|
if ($st.State -eq 'removed') {
|
||||||
|
$out += "Поддержка: снята с поддержки полностью"
|
||||||
|
return $out
|
||||||
|
}
|
||||||
|
$out += "Поддержка: на поддержке"
|
||||||
|
if ($st.G -eq 0) {
|
||||||
|
$out += " Возможность изменения: включена"
|
||||||
|
$out += " Объектов: на замке $($st.Counts[0]) / редактируется $($st.Counts[1]) / снято $($st.Counts[2])"
|
||||||
|
} else {
|
||||||
|
$out += " Возможность изменения: выключена — вся конфигурация read-only (правки заблокированы)"
|
||||||
|
}
|
||||||
|
$out += " Конфигураций поставщика: $($st.K)"
|
||||||
|
if ($st.K -gt 1) {
|
||||||
|
foreach ($v in $st.Vendors) { $out += " Поставщик: $($v.Vendor) — $($v.Name) $($v.Version)" }
|
||||||
|
}
|
||||||
|
return $out
|
||||||
|
}
|
||||||
|
|
||||||
|
function Format-HomePageItem($it, [bool]$detailed) {
|
||||||
|
$badges = @()
|
||||||
|
$badges += "h=$($it.height)"
|
||||||
|
if (-not $it.common) { $badges += "скрыта" }
|
||||||
|
if ($it.roles.Count -gt 0) {
|
||||||
|
if ($detailed) { $badges += "роли: $($it.roles.Count)" }
|
||||||
|
else { $badges += "+$($it.roles.Count) ролей" }
|
||||||
|
}
|
||||||
|
$tail = if ($badges.Count -gt 0) { " (" + ($badges -join ", ") + ")" } else { "" }
|
||||||
|
return " $($it.form)$tail"
|
||||||
|
}
|
||||||
|
|
||||||
# --- Count objects in ChildObjects ---
|
# --- Count objects in ChildObjects ---
|
||||||
$objectCounts = [ordered]@{}
|
$objectCounts = [ordered]@{}
|
||||||
$totalObjects = 0
|
$totalObjects = 0
|
||||||
@@ -141,6 +325,7 @@ $cfgVersion = Get-PropText "Version"
|
|||||||
$cfgVendor = Get-PropText "Vendor"
|
$cfgVendor = Get-PropText "Vendor"
|
||||||
$cfgCompat = Get-PropText "CompatibilityMode"
|
$cfgCompat = Get-PropText "CompatibilityMode"
|
||||||
$cfgExtCompat = Get-PropText "ConfigurationExtensionCompatibilityMode"
|
$cfgExtCompat = Get-PropText "ConfigurationExtensionCompatibilityMode"
|
||||||
|
$cfgExtPurpose = Get-PropText "ConfigurationExtensionPurpose"
|
||||||
$cfgDefaultRun = Get-PropText "DefaultRunMode"
|
$cfgDefaultRun = Get-PropText "DefaultRunMode"
|
||||||
$cfgScript = Get-PropText "ScriptVariant"
|
$cfgScript = Get-PropText "ScriptVariant"
|
||||||
$cfgDefaultLang = Get-PropText "DefaultLanguage"
|
$cfgDefaultLang = Get-PropText "DefaultLanguage"
|
||||||
@@ -154,7 +339,7 @@ $cfgDbSpaces = Get-PropText "DatabaseTablespacesUseMode"
|
|||||||
$cfgWindowMode = Get-PropText "MainClientApplicationWindowMode"
|
$cfgWindowMode = Get-PropText "MainClientApplicationWindowMode"
|
||||||
|
|
||||||
# --- BRIEF mode ---
|
# --- BRIEF mode ---
|
||||||
if ($Mode -eq "brief") {
|
if ($Mode -eq "brief" -and -not $Section) {
|
||||||
$synPart = if ($cfgSynonym) { " $dash `"$cfgSynonym`"" } else { "" }
|
$synPart = if ($cfgSynonym) { " $dash `"$cfgSynonym`"" } else { "" }
|
||||||
$verPart = if ($cfgVersion) { " v$cfgVersion" } else { "" }
|
$verPart = if ($cfgVersion) { " v$cfgVersion" } else { "" }
|
||||||
$compatPart = if ($cfgCompat) { " | $cfgCompat" } else { "" }
|
$compatPart = if ($cfgCompat) { " | $cfgCompat" } else { "" }
|
||||||
@@ -162,7 +347,7 @@ if ($Mode -eq "brief") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- OVERVIEW mode ---
|
# --- OVERVIEW mode ---
|
||||||
if ($Mode -eq "overview") {
|
if ($Mode -eq "overview" -and -not $Section) {
|
||||||
$synPart = if ($cfgSynonym) { " $dash `"$cfgSynonym`"" } else { "" }
|
$synPart = if ($cfgSynonym) { " $dash `"$cfgSynonym`"" } else { "" }
|
||||||
$verPart = if ($cfgVersion) { " v$cfgVersion" } else { "" }
|
$verPart = if ($cfgVersion) { " v$cfgVersion" } else { "" }
|
||||||
Out "=== Конфигурация: ${cfgName}${synPart}${verPart} ==="
|
Out "=== Конфигурация: ${cfgName}${synPart}${verPart} ==="
|
||||||
@@ -172,6 +357,7 @@ if ($Mode -eq "overview") {
|
|||||||
Out "Формат: $version"
|
Out "Формат: $version"
|
||||||
if ($cfgVendor) { Out "Поставщик: $cfgVendor" }
|
if ($cfgVendor) { Out "Поставщик: $cfgVendor" }
|
||||||
if ($cfgVersion) { Out "Версия: $cfgVersion" }
|
if ($cfgVersion) { Out "Версия: $cfgVersion" }
|
||||||
|
foreach ($l in (Get-SupportLines)) { Out $l }
|
||||||
Out "Совместимость: $cfgCompat"
|
Out "Совместимость: $cfgCompat"
|
||||||
Out "Режим запуска: $cfgDefaultRun"
|
Out "Режим запуска: $cfgDefaultRun"
|
||||||
Out "Язык скриптов: $cfgScript"
|
Out "Язык скриптов: $cfgScript"
|
||||||
@@ -181,6 +367,33 @@ if ($Mode -eq "overview") {
|
|||||||
Out "Интерфейс: $cfgIntfCompat"
|
Out "Интерфейс: $cfgIntfCompat"
|
||||||
Out ""
|
Out ""
|
||||||
|
|
||||||
|
# Panel layout (if file exists)
|
||||||
|
if ($script:panelLayout) {
|
||||||
|
$hasPlaced = $false
|
||||||
|
foreach ($s in @("top","left","right","bottom")) {
|
||||||
|
if ($script:panelLayout[$s].Count -gt 0) { $hasPlaced = $true; break }
|
||||||
|
}
|
||||||
|
if ($hasPlaced) {
|
||||||
|
Out "--- Раскладка панелей ---"
|
||||||
|
foreach ($s in @("top","left","right","bottom")) {
|
||||||
|
if ($script:panelLayout[$s].Count -gt 0) {
|
||||||
|
Out " $($s.PadRight(7)) $(Format-LayoutSlots $script:panelLayout[$s])"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Out ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Home page layout (brief summary)
|
||||||
|
if ($script:homePage) {
|
||||||
|
$ln = $script:homePage.left.Count
|
||||||
|
$rn = $script:homePage.right.Count
|
||||||
|
Out "--- Начальная страница ---"
|
||||||
|
Out " Шаблон: $($script:homePage.template)"
|
||||||
|
Out " LeftColumn: $ln, RightColumn: $rn (детали: -Section home-page)"
|
||||||
|
Out ""
|
||||||
|
}
|
||||||
|
|
||||||
# Object counts table
|
# Object counts table
|
||||||
Out "--- Состав ($totalObjects объектов) ---"
|
Out "--- Состав ($totalObjects объектов) ---"
|
||||||
Out ""
|
Out ""
|
||||||
@@ -203,8 +416,34 @@ if ($Mode -eq "overview") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Drill-down: -Section home-page ---
|
||||||
|
if ($Section -eq "home-page") {
|
||||||
|
if (-not $script:homePage) {
|
||||||
|
Out "Файл Ext/HomePageWorkArea.xml не найден"
|
||||||
|
} else {
|
||||||
|
Out "=== Начальная страница: $cfgName ==="
|
||||||
|
Out ""
|
||||||
|
Out "Шаблон: $($script:homePage.template)"
|
||||||
|
Out ""
|
||||||
|
foreach ($side in @(@("LeftColumn","left"), @("RightColumn","right"))) {
|
||||||
|
$items = $script:homePage[$side[1]]
|
||||||
|
$lbl = $side[0]
|
||||||
|
if ($items.Count -eq 0) { Out "${lbl}: —"; Out ""; continue }
|
||||||
|
Out "${lbl} ($($items.Count)):"
|
||||||
|
foreach ($it in $items) {
|
||||||
|
Out (Format-HomePageItem $it $true)
|
||||||
|
foreach ($r in $it.roles) {
|
||||||
|
$rval = if ($r.value) { "true" } else { "false" }
|
||||||
|
Out " $($r.name): $rval"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Out ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# --- FULL mode ---
|
# --- FULL mode ---
|
||||||
if ($Mode -eq "full") {
|
if ($Mode -eq "full" -and -not $Section) {
|
||||||
$synPart = if ($cfgSynonym) { " $dash `"$cfgSynonym`"" } else { "" }
|
$synPart = if ($cfgSynonym) { " $dash `"$cfgSynonym`"" } else { "" }
|
||||||
$verPart = if ($cfgVersion) { " v$cfgVersion" } else { "" }
|
$verPart = if ($cfgVersion) { " v$cfgVersion" } else { "" }
|
||||||
Out "=== Конфигурация: ${cfgName}${synPart}${verPart} ==="
|
Out "=== Конфигурация: ${cfgName}${synPart}${verPart} ==="
|
||||||
@@ -221,6 +460,7 @@ if ($Mode -eq "full") {
|
|||||||
if ($cfgPrefix) { Out "Префикс: $cfgPrefix" }
|
if ($cfgPrefix) { Out "Префикс: $cfgPrefix" }
|
||||||
if ($cfgVendor) { Out "Поставщик: $cfgVendor" }
|
if ($cfgVendor) { Out "Поставщик: $cfgVendor" }
|
||||||
if ($cfgVersion) { Out "Версия: $cfgVersion" }
|
if ($cfgVersion) { Out "Версия: $cfgVersion" }
|
||||||
|
foreach ($l in (Get-SupportLines)) { Out $l }
|
||||||
$cfgUpdateAddr = Get-PropText "UpdateCatalogAddress"
|
$cfgUpdateAddr = Get-PropText "UpdateCatalogAddress"
|
||||||
if ($cfgUpdateAddr) { Out "Каталог обн.: $cfgUpdateAddr" }
|
if ($cfgUpdateAddr) { Out "Каталог обн.: $cfgUpdateAddr" }
|
||||||
Out ""
|
Out ""
|
||||||
@@ -275,6 +515,33 @@ if ($Mode -eq "full") {
|
|||||||
Out "Обычн.формы в управл.: $useOF"
|
Out "Обычн.формы в управл.: $useOF"
|
||||||
Out ""
|
Out ""
|
||||||
|
|
||||||
|
# --- Section: Panel layout ---
|
||||||
|
if ($script:panelLayout) {
|
||||||
|
Out "--- Раскладка панелей ---"
|
||||||
|
foreach ($s in @("top","left","right","bottom")) {
|
||||||
|
$slots = $script:panelLayout[$s]
|
||||||
|
if ($slots.Count -gt 0) {
|
||||||
|
Out " $($s.PadRight(7)) $(Format-LayoutSlots $slots)"
|
||||||
|
} else {
|
||||||
|
Out " $($s.PadRight(7)) —"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($script:panelLayout.declared.Count -gt 0) {
|
||||||
|
Out " объявлено: $($script:panelLayout.declared -join ', ')"
|
||||||
|
}
|
||||||
|
Out ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Section: Home page (brief summary) ---
|
||||||
|
if ($script:homePage) {
|
||||||
|
$ln = $script:homePage.left.Count
|
||||||
|
$rn = $script:homePage.right.Count
|
||||||
|
Out "--- Начальная страница ---"
|
||||||
|
Out " Шаблон: $($script:homePage.template)"
|
||||||
|
Out " LeftColumn: $ln, RightColumn: $rn (детали: -Section home-page)"
|
||||||
|
Out ""
|
||||||
|
}
|
||||||
|
|
||||||
# --- Section: Storages & default forms ---
|
# --- Section: Storages & default forms ---
|
||||||
Out "--- Хранилища и формы по умолчанию ---"
|
Out "--- Хранилища и формы по умолчанию ---"
|
||||||
$storageProps = @("CommonSettingsStorage","ReportsUserSettingsStorage","ReportsVariantsStorage","FormDataSettingsStorage","DynamicListsUserSettingsStorage","URLExternalDataStorage")
|
$storageProps = @("CommonSettingsStorage","ReportsUserSettingsStorage","ReportsVariantsStorage","FormDataSettingsStorage","DynamicListsUserSettingsStorage","URLExternalDataStorage")
|
||||||
+236
-5
@@ -1,9 +1,10 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cf-info v1.0 — Compact summary of 1C configuration root
|
# cf-info v1.3 — Compact summary of 1C configuration root
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
@@ -13,8 +14,9 @@ sys.stderr.reconfigure(encoding="utf-8")
|
|||||||
|
|
||||||
# --- Argument parsing ---
|
# --- Argument parsing ---
|
||||||
parser = argparse.ArgumentParser(description="Analyze 1C configuration structure", allow_abbrev=False)
|
parser = argparse.ArgumentParser(description="Analyze 1C configuration structure", allow_abbrev=False)
|
||||||
parser.add_argument("-ConfigPath", required=True, help="Path to Configuration.xml or directory")
|
parser.add_argument("-ConfigPath", "-Path", required=True, help="Path to Configuration.xml or directory")
|
||||||
parser.add_argument("-Mode", choices=["overview", "brief", "full"], default="overview", help="Output mode")
|
parser.add_argument("-Mode", choices=["overview", "brief", "full"], default="overview", help="Output mode")
|
||||||
|
parser.add_argument("-Section", "-Name", choices=["home-page"], default=None, help="Drill-down section (alias: -Name)")
|
||||||
parser.add_argument("-Limit", type=int, default=150, help="Max lines to show")
|
parser.add_argument("-Limit", type=int, default=150, help="Max lines to show")
|
||||||
parser.add_argument("-Offset", type=int, default=0, help="Lines to skip")
|
parser.add_argument("-Offset", type=int, default=0, help="Lines to skip")
|
||||||
parser.add_argument("-OutFile", default="", help="Write output to file")
|
parser.add_argument("-OutFile", default="", help="Write output to file")
|
||||||
@@ -125,6 +127,173 @@ type_ru_names = {
|
|||||||
"Task": "Задачи", "IntegrationService": "Сервисы интеграции",
|
"Task": "Задачи", "IntegrationService": "Сервисы интеграции",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Read panel layout (Ext/ClientApplicationInterface.xml) ---
|
||||||
|
PANEL_NAMES = {
|
||||||
|
"cbab57f2-a0f3-4f0a-89ea-4cb19570ab75": "Открытых",
|
||||||
|
"b553047f-c9aa-4157-978d-448ecad24248": "Разделов",
|
||||||
|
"13322b22-3960-4d68-93a6-fe2dd7f28ca3": "Избранного",
|
||||||
|
"c933ac92-92cd-459d-81cc-e0c8a83ced99": "История",
|
||||||
|
"b2735bd3-d822-4430-ba59-c9e869693b24": "Функций",
|
||||||
|
}
|
||||||
|
CAI_NS = "http://v8.1c.ru/8.2/managed-application/core"
|
||||||
|
|
||||||
|
def get_panels_layout():
|
||||||
|
cfg_dir = os.path.dirname(config_path)
|
||||||
|
cai_path = os.path.join(cfg_dir, "Ext", "ClientApplicationInterface.xml")
|
||||||
|
if not os.path.isfile(cai_path):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
cai_tree = etree.parse(cai_path)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
cai_root = cai_tree.getroot()
|
||||||
|
layout = {"top": [], "left": [], "right": [], "bottom": [], "declared": []}
|
||||||
|
for side in ("top", "left", "right", "bottom"):
|
||||||
|
for side_el in cai_root.findall(f"{{{CAI_NS}}}{side}"):
|
||||||
|
slot = []
|
||||||
|
for u in side_el.iter(f"{{{CAI_NS}}}uuid"):
|
||||||
|
key = (u.text or "").strip()
|
||||||
|
slot.append(PANEL_NAMES.get(key, f"?{key}"))
|
||||||
|
if slot:
|
||||||
|
layout[side].append(slot)
|
||||||
|
for pd in cai_root.findall(f"{{{CAI_NS}}}panelDef"):
|
||||||
|
key = pd.get("id", "")
|
||||||
|
layout["declared"].append(PANEL_NAMES.get(key, f"?{key}"))
|
||||||
|
return layout
|
||||||
|
|
||||||
|
def format_layout_slots(slots):
|
||||||
|
if not slots:
|
||||||
|
return ""
|
||||||
|
parts = []
|
||||||
|
for slot in slots:
|
||||||
|
if len(slot) == 1:
|
||||||
|
parts.append(slot[0])
|
||||||
|
else:
|
||||||
|
parts.append("Стек(" + ", ".join(slot) + ")")
|
||||||
|
return " | ".join(parts)
|
||||||
|
|
||||||
|
panel_layout = get_panels_layout()
|
||||||
|
|
||||||
|
# --- Read home page layout (Ext/HomePageWorkArea.xml) ---
|
||||||
|
HP_NS = "http://v8.1c.ru/8.3/xcf/extrnprops"
|
||||||
|
XR_NS_HP = "http://v8.1c.ru/8.3/xcf/readable"
|
||||||
|
|
||||||
|
def get_home_page_layout():
|
||||||
|
cfg_dir = os.path.dirname(config_path)
|
||||||
|
hp_path = os.path.join(cfg_dir, "Ext", "HomePageWorkArea.xml")
|
||||||
|
if not os.path.isfile(hp_path):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
hp_tree = etree.parse(hp_path)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
hp_root = hp_tree.getroot()
|
||||||
|
result = {"template": "", "left": [], "right": []}
|
||||||
|
tn = hp_root.find(f"{{{HP_NS}}}WorkingAreaTemplate")
|
||||||
|
if tn is not None and tn.text:
|
||||||
|
result["template"] = tn.text.strip()
|
||||||
|
for col_name, key in (("LeftColumn", "left"), ("RightColumn", "right")):
|
||||||
|
col = hp_root.find(f"{{{HP_NS}}}{col_name}")
|
||||||
|
if col is None:
|
||||||
|
continue
|
||||||
|
items = []
|
||||||
|
for it in col.findall(f"{{{HP_NS}}}Item"):
|
||||||
|
f = it.find(f"{{{HP_NS}}}Form")
|
||||||
|
h = it.find(f"{{{HP_NS}}}Height")
|
||||||
|
vis = it.find(f"{{{HP_NS}}}Visibility")
|
||||||
|
common = True
|
||||||
|
roles = []
|
||||||
|
if vis is not None:
|
||||||
|
cn = vis.find(f"{{{XR_NS_HP}}}Common")
|
||||||
|
if cn is not None and cn.text:
|
||||||
|
common = cn.text.strip() == "true"
|
||||||
|
for v in vis.findall(f"{{{XR_NS_HP}}}Value"):
|
||||||
|
roles.append({"name": v.get("name", ""), "value": (v.text or "").strip() == "true"})
|
||||||
|
items.append({
|
||||||
|
"form": (f.text or "").strip() if f is not None else "",
|
||||||
|
"height": int((h.text or "10").strip()) if h is not None else 10,
|
||||||
|
"common": common,
|
||||||
|
"roles": roles,
|
||||||
|
})
|
||||||
|
result[key] = items
|
||||||
|
return result
|
||||||
|
|
||||||
|
home_page = get_home_page_layout()
|
||||||
|
|
||||||
|
# --- Support state (Ext/ParentConfigurations.bin) ---
|
||||||
|
# Decodes the 1C support-state file. See docs/1c-support-state-spec.md.
|
||||||
|
# Returns None on absent/error; else dict: state='absent'|'removed'|'parsed',
|
||||||
|
# g (0=editing on, 1=off), k (vendor configs), vendors [{vendor,name,version}],
|
||||||
|
# counts [locked, editable, removed] by f1 — record tally (k>1 counts each
|
||||||
|
# vendor block separately); only computed when g==0.
|
||||||
|
def read_support_state(bin_path):
|
||||||
|
try:
|
||||||
|
if not os.path.isfile(bin_path):
|
||||||
|
return {"state": "absent"}
|
||||||
|
data = open(bin_path, "rb").read()
|
||||||
|
if len(data) <= 32:
|
||||||
|
return {"state": "removed"}
|
||||||
|
if data[:3] == b"\xef\xbb\xbf":
|
||||||
|
data = data[3:]
|
||||||
|
text = data.decode("utf-8", "replace")
|
||||||
|
h = re.match(r"\{6,(\d+),(\d+),", text)
|
||||||
|
if not h:
|
||||||
|
return None
|
||||||
|
g = int(h.group(1))
|
||||||
|
k = int(h.group(2))
|
||||||
|
if k == 0:
|
||||||
|
return {"state": "removed"}
|
||||||
|
vendors = []
|
||||||
|
for m in re.finditer(r'"((?:[^"]|"")*)","((?:[^"]|"")*)","((?:[^"]|"")*)",\d+,', text):
|
||||||
|
vendors.append({
|
||||||
|
"version": m.group(1).replace('""', '"'),
|
||||||
|
"vendor": m.group(2).replace('""', '"'),
|
||||||
|
"name": m.group(3).replace('""', '"'),
|
||||||
|
})
|
||||||
|
counts = None
|
||||||
|
if g == 0:
|
||||||
|
counts = [0, 0, 0]
|
||||||
|
for m in re.finditer(r"([0-2]),0,[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", text):
|
||||||
|
counts[int(m.group(1))] += 1
|
||||||
|
return {"state": "parsed", "g": g, "k": k, "vendors": vendors, "counts": counts}
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_support_lines():
|
||||||
|
config_dir = os.path.dirname(config_path)
|
||||||
|
bin_path = os.path.join(config_dir, "Ext", "ParentConfigurations.bin")
|
||||||
|
st = read_support_state(bin_path)
|
||||||
|
res = []
|
||||||
|
if not st or st["state"] == "absent":
|
||||||
|
if cfg_ext_purpose:
|
||||||
|
res.append("Поддержка: расширение (CFE), правки свободны")
|
||||||
|
else:
|
||||||
|
res.append("Поддержка: не на поддержке (своя конфигурация)")
|
||||||
|
return res
|
||||||
|
if st["state"] == "removed":
|
||||||
|
res.append("Поддержка: снята с поддержки полностью")
|
||||||
|
return res
|
||||||
|
res.append("Поддержка: на поддержке")
|
||||||
|
if st["g"] == 0:
|
||||||
|
res.append(" Возможность изменения: включена")
|
||||||
|
res.append(f" Объектов: на замке {st['counts'][0]} / редактируется {st['counts'][1]} / снято {st['counts'][2]}")
|
||||||
|
else:
|
||||||
|
res.append(" Возможность изменения: выключена — вся конфигурация read-only (правки заблокированы)")
|
||||||
|
res.append(f" Конфигураций поставщика: {st['k']}")
|
||||||
|
if st["k"] > 1:
|
||||||
|
for v in st["vendors"]:
|
||||||
|
res.append(f" Поставщик: {v['vendor']} — {v['name']} {v['version']}")
|
||||||
|
return res
|
||||||
|
|
||||||
|
def format_home_page_item(it, detailed):
|
||||||
|
badges = [f"h={it['height']}"]
|
||||||
|
if not it["common"]:
|
||||||
|
badges.append("скрыта")
|
||||||
|
if it["roles"]:
|
||||||
|
badges.append(f"роли: {len(it['roles'])}" if detailed else f"+{len(it['roles'])} ролей")
|
||||||
|
tail = f" ({', '.join(badges)})" if badges else ""
|
||||||
|
return f" {it['form']}{tail}"
|
||||||
|
|
||||||
# --- Count objects in ChildObjects ---
|
# --- Count objects in ChildObjects ---
|
||||||
object_counts = OrderedDict()
|
object_counts = OrderedDict()
|
||||||
total_objects = 0
|
total_objects = 0
|
||||||
@@ -146,6 +315,7 @@ cfg_version = get_prop_text("Version")
|
|||||||
cfg_vendor = get_prop_text("Vendor")
|
cfg_vendor = get_prop_text("Vendor")
|
||||||
cfg_compat = get_prop_text("CompatibilityMode")
|
cfg_compat = get_prop_text("CompatibilityMode")
|
||||||
cfg_ext_compat = get_prop_text("ConfigurationExtensionCompatibilityMode")
|
cfg_ext_compat = get_prop_text("ConfigurationExtensionCompatibilityMode")
|
||||||
|
cfg_ext_purpose = get_prop_text("ConfigurationExtensionPurpose")
|
||||||
cfg_default_run = get_prop_text("DefaultRunMode")
|
cfg_default_run = get_prop_text("DefaultRunMode")
|
||||||
cfg_script = get_prop_text("ScriptVariant")
|
cfg_script = get_prop_text("ScriptVariant")
|
||||||
cfg_default_lang = get_prop_text("DefaultLanguage")
|
cfg_default_lang = get_prop_text("DefaultLanguage")
|
||||||
@@ -159,14 +329,14 @@ cfg_db_spaces = get_prop_text("DatabaseTablespacesUseMode")
|
|||||||
cfg_window_mode = get_prop_text("MainClientApplicationWindowMode")
|
cfg_window_mode = get_prop_text("MainClientApplicationWindowMode")
|
||||||
|
|
||||||
# --- BRIEF mode ---
|
# --- BRIEF mode ---
|
||||||
if args.Mode == "brief":
|
if args.Mode == "brief" and not args.Section:
|
||||||
syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else ""
|
syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else ""
|
||||||
ver_part = f" v{cfg_version}" if cfg_version else ""
|
ver_part = f" v{cfg_version}" if cfg_version else ""
|
||||||
compat_part = f" | {cfg_compat}" if cfg_compat else ""
|
compat_part = f" | {cfg_compat}" if cfg_compat else ""
|
||||||
out(f"Конфигурация: {cfg_name}{syn_part}{ver_part} | {total_objects} объектов{compat_part}")
|
out(f"Конфигурация: {cfg_name}{syn_part}{ver_part} | {total_objects} объектов{compat_part}")
|
||||||
|
|
||||||
# --- OVERVIEW mode ---
|
# --- OVERVIEW mode ---
|
||||||
if args.Mode == "overview":
|
if args.Mode == "overview" and not args.Section:
|
||||||
syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else ""
|
syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else ""
|
||||||
ver_part = f" v{cfg_version}" if cfg_version else ""
|
ver_part = f" v{cfg_version}" if cfg_version else ""
|
||||||
out(f"=== Конфигурация: {cfg_name}{syn_part}{ver_part} ===")
|
out(f"=== Конфигурация: {cfg_name}{syn_part}{ver_part} ===")
|
||||||
@@ -178,6 +348,8 @@ if args.Mode == "overview":
|
|||||||
out(f"Поставщик: {cfg_vendor}")
|
out(f"Поставщик: {cfg_vendor}")
|
||||||
if cfg_version:
|
if cfg_version:
|
||||||
out(f"Версия: {cfg_version}")
|
out(f"Версия: {cfg_version}")
|
||||||
|
for ln in get_support_lines():
|
||||||
|
out(ln)
|
||||||
out(f"Совместимость: {cfg_compat}")
|
out(f"Совместимость: {cfg_compat}")
|
||||||
out(f"Режим запуска: {cfg_default_run}")
|
out(f"Режим запуска: {cfg_default_run}")
|
||||||
out(f"Язык скриптов: {cfg_script}")
|
out(f"Язык скриптов: {cfg_script}")
|
||||||
@@ -187,6 +359,20 @@ if args.Mode == "overview":
|
|||||||
out(f"Интерфейс: {cfg_intf_compat}")
|
out(f"Интерфейс: {cfg_intf_compat}")
|
||||||
out()
|
out()
|
||||||
|
|
||||||
|
if panel_layout and any(panel_layout[s] for s in ("top", "left", "right", "bottom")):
|
||||||
|
out("--- Раскладка панелей ---")
|
||||||
|
for s in ("top", "left", "right", "bottom"):
|
||||||
|
if panel_layout[s]:
|
||||||
|
out(f" {s.ljust(7)} {format_layout_slots(panel_layout[s])}")
|
||||||
|
out()
|
||||||
|
|
||||||
|
# Home page (brief summary)
|
||||||
|
if home_page:
|
||||||
|
out("--- Начальная страница ---")
|
||||||
|
out(f" Шаблон: {home_page['template']}")
|
||||||
|
out(f" LeftColumn: {len(home_page['left'])}, RightColumn: {len(home_page['right'])} (детали: -Section home-page)")
|
||||||
|
out()
|
||||||
|
|
||||||
# Object counts table
|
# Object counts table
|
||||||
out(f"--- Состав ({total_objects} объектов) ---")
|
out(f"--- Состав ({total_objects} объектов) ---")
|
||||||
out()
|
out()
|
||||||
@@ -207,7 +393,30 @@ if args.Mode == "overview":
|
|||||||
out(f" {padded} {count}")
|
out(f" {padded} {count}")
|
||||||
|
|
||||||
# --- FULL mode ---
|
# --- FULL mode ---
|
||||||
if args.Mode == "full":
|
# --- Drill-down: -Section home-page ---
|
||||||
|
if args.Section == "home-page":
|
||||||
|
if not home_page:
|
||||||
|
out("Файл Ext/HomePageWorkArea.xml не найден")
|
||||||
|
else:
|
||||||
|
out(f"=== Начальная страница: {cfg_name} ===")
|
||||||
|
out()
|
||||||
|
out(f"Шаблон: {home_page['template']}")
|
||||||
|
out()
|
||||||
|
for col_lbl, col_key in (("LeftColumn", "left"), ("RightColumn", "right")):
|
||||||
|
items = home_page[col_key]
|
||||||
|
if not items:
|
||||||
|
out(f"{col_lbl}: —")
|
||||||
|
out()
|
||||||
|
continue
|
||||||
|
out(f"{col_lbl} ({len(items)}):")
|
||||||
|
for it in items:
|
||||||
|
out(format_home_page_item(it, True))
|
||||||
|
for r in it["roles"]:
|
||||||
|
rval = "true" if r["value"] else "false"
|
||||||
|
out(f" {r['name']}: {rval}")
|
||||||
|
out()
|
||||||
|
|
||||||
|
if args.Mode == "full" and not args.Section:
|
||||||
syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else ""
|
syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else ""
|
||||||
ver_part = f" v{cfg_version}" if cfg_version else ""
|
ver_part = f" v{cfg_version}" if cfg_version else ""
|
||||||
out(f"=== Конфигурация: {cfg_name}{syn_part}{ver_part} ===")
|
out(f"=== Конфигурация: {cfg_name}{syn_part}{ver_part} ===")
|
||||||
@@ -229,6 +438,8 @@ if args.Mode == "full":
|
|||||||
out(f"Поставщик: {cfg_vendor}")
|
out(f"Поставщик: {cfg_vendor}")
|
||||||
if cfg_version:
|
if cfg_version:
|
||||||
out(f"Версия: {cfg_version}")
|
out(f"Версия: {cfg_version}")
|
||||||
|
for ln in get_support_lines():
|
||||||
|
out(ln)
|
||||||
cfg_update_addr = get_prop_text("UpdateCatalogAddress")
|
cfg_update_addr = get_prop_text("UpdateCatalogAddress")
|
||||||
if cfg_update_addr:
|
if cfg_update_addr:
|
||||||
out(f"Каталог обн.: {cfg_update_addr}")
|
out(f"Каталог обн.: {cfg_update_addr}")
|
||||||
@@ -283,6 +494,26 @@ if args.Mode == "full":
|
|||||||
out(f"Обычн.формы в управл.: {use_of}")
|
out(f"Обычн.формы в управл.: {use_of}")
|
||||||
out()
|
out()
|
||||||
|
|
||||||
|
# --- Section: Panel layout ---
|
||||||
|
if panel_layout:
|
||||||
|
out("--- Раскладка панелей ---")
|
||||||
|
for s in ("top", "left", "right", "bottom"):
|
||||||
|
slots = panel_layout[s]
|
||||||
|
if slots:
|
||||||
|
out(f" {s.ljust(7)} {format_layout_slots(slots)}")
|
||||||
|
else:
|
||||||
|
out(f" {s.ljust(7)} —")
|
||||||
|
if panel_layout["declared"]:
|
||||||
|
out(f" объявлено: {', '.join(panel_layout['declared'])}")
|
||||||
|
out()
|
||||||
|
|
||||||
|
# --- Section: Home page (brief summary) ---
|
||||||
|
if home_page:
|
||||||
|
out("--- Начальная страница ---")
|
||||||
|
out(f" Шаблон: {home_page['template']}")
|
||||||
|
out(f" LeftColumn: {len(home_page['left'])}, RightColumn: {len(home_page['right'])} (детали: -Section home-page)")
|
||||||
|
out()
|
||||||
|
|
||||||
# --- Section: Storages & default forms ---
|
# --- Section: Storages & default forms ---
|
||||||
out("--- Хранилища и формы по умолчанию ---")
|
out("--- Хранилища и формы по умолчанию ---")
|
||||||
storage_props = [
|
storage_props = [
|
||||||
@@ -24,16 +24,7 @@ allowed-tools:
|
|||||||
| `CompatibilityMode` | Режим совместимости (default: `Version8_3_24`) |
|
| `CompatibilityMode` | Режим совместимости (default: `Version8_3_24`) |
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File .claude/skills/cf-init/scripts/cf-init.ps1 -Name "МояКонфигурация"
|
python ".opencode/skills/cf-init/scripts/cf-init.py" -Name "МояКонфигурация"
|
||||||
```
|
|
||||||
|
|
||||||
## Что создаётся
|
|
||||||
|
|
||||||
```
|
|
||||||
<OutputDir>/
|
|
||||||
├── Configuration.xml # Корневой файл — все свойства
|
|
||||||
└── Languages/
|
|
||||||
└── Русский.xml # Язык по умолчанию
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
+36
-2
@@ -1,4 +1,4 @@
|
|||||||
# cf-init v1.0 — Create empty 1C configuration scaffold
|
# cf-init v1.2 — Create empty 1C configuration scaffold
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -160,7 +160,7 @@ $cfgXml = @"
|
|||||||
<ObjectAutonumerationMode>NotAutoFree</ObjectAutonumerationMode>
|
<ObjectAutonumerationMode>NotAutoFree</ObjectAutonumerationMode>
|
||||||
<ModalityUseMode>DontUse</ModalityUseMode>
|
<ModalityUseMode>DontUse</ModalityUseMode>
|
||||||
<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode>
|
<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode>
|
||||||
<InterfaceCompatibilityMode>Taxi</InterfaceCompatibilityMode>
|
<InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode>
|
||||||
<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode>
|
<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode>
|
||||||
<CompatibilityMode>$CompatibilityMode</CompatibilityMode>
|
<CompatibilityMode>$CompatibilityMode</CompatibilityMode>
|
||||||
<DefaultConstantsForm/>
|
<DefaultConstantsForm/>
|
||||||
@@ -192,6 +192,33 @@ $langXml = @"
|
|||||||
</MetaDataObject>
|
</MetaDataObject>
|
||||||
"@
|
"@
|
||||||
|
|
||||||
|
# --- Ext/ClientApplicationInterface.xml (default ERP-style panel layout) ---
|
||||||
|
# Open panel on top, Sections panel on left; Functions/Favorites/History declared
|
||||||
|
# via panelDef but not placed by default. Without this file the web client renders
|
||||||
|
# section icons without labels (icon-only mode).
|
||||||
|
$openPanelInst = [guid]::NewGuid().ToString()
|
||||||
|
$sectionsPanelInst = [guid]::NewGuid().ToString()
|
||||||
|
$caiXml = @"
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ClientApplicationInterface xmlns="http://v8.1c.ru/8.2/managed-application/core" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="InterfaceLayouter">
|
||||||
|
<top>
|
||||||
|
<panel id="$openPanelInst">
|
||||||
|
<uuid>cbab57f2-a0f3-4f0a-89ea-4cb19570ab75</uuid>
|
||||||
|
</panel>
|
||||||
|
</top>
|
||||||
|
<left>
|
||||||
|
<panel id="$sectionsPanelInst">
|
||||||
|
<uuid>b553047f-c9aa-4157-978d-448ecad24248</uuid>
|
||||||
|
</panel>
|
||||||
|
</left>
|
||||||
|
<panelDef id="b553047f-c9aa-4157-978d-448ecad24248"/>
|
||||||
|
<panelDef id="13322b22-3960-4d68-93a6-fe2dd7f28ca3"/>
|
||||||
|
<panelDef id="c933ac92-92cd-459d-81cc-e0c8a83ced99"/>
|
||||||
|
<panelDef id="cbab57f2-a0f3-4f0a-89ea-4cb19570ab75"/>
|
||||||
|
<panelDef id="b2735bd3-d822-4430-ba59-c9e869693b24"/>
|
||||||
|
</ClientApplicationInterface>
|
||||||
|
"@
|
||||||
|
|
||||||
# --- Create directories ---
|
# --- Create directories ---
|
||||||
if (-not (Test-Path $OutputDir)) {
|
if (-not (Test-Path $OutputDir)) {
|
||||||
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
|
||||||
@@ -200,6 +227,10 @@ $langDir = Join-Path $OutputDir "Languages"
|
|||||||
if (-not (Test-Path $langDir)) {
|
if (-not (Test-Path $langDir)) {
|
||||||
New-Item -ItemType Directory -Path $langDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $langDir -Force | Out-Null
|
||||||
}
|
}
|
||||||
|
$extDir = Join-Path $OutputDir "Ext"
|
||||||
|
if (-not (Test-Path $extDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $extDir -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
# --- Write files with UTF-8 BOM ---
|
# --- Write files with UTF-8 BOM ---
|
||||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||||
@@ -207,9 +238,12 @@ $enc = New-Object System.Text.UTF8Encoding($true)
|
|||||||
[System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc)
|
[System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc)
|
||||||
$langFile = Join-Path $langDir "Русский.xml"
|
$langFile = Join-Path $langDir "Русский.xml"
|
||||||
[System.IO.File]::WriteAllText($langFile, $langXml, $enc)
|
[System.IO.File]::WriteAllText($langFile, $langXml, $enc)
|
||||||
|
$caiFile = Join-Path $extDir "ClientApplicationInterface.xml"
|
||||||
|
[System.IO.File]::WriteAllText($caiFile, $caiXml, $enc)
|
||||||
|
|
||||||
# --- Output ---
|
# --- Output ---
|
||||||
Write-Host "[OK] Создана конфигурация: $Name"
|
Write-Host "[OK] Создана конфигурация: $Name"
|
||||||
Write-Host " Каталог: $OutputDir"
|
Write-Host " Каталог: $OutputDir"
|
||||||
Write-Host " Configuration.xml: $cfgFile"
|
Write-Host " Configuration.xml: $cfgFile"
|
||||||
Write-Host " Languages: $langFile"
|
Write-Host " Languages: $langFile"
|
||||||
|
Write-Host " Ext/CAI: $caiFile"
|
||||||
+32
-2
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cf-init v1.0 — Create empty 1C configuration scaffold
|
# cf-init v1.2 — Create empty 1C configuration scaffold
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
"""Generates minimal XML source files for a 1C configuration."""
|
"""Generates minimal XML source files for a 1C configuration."""
|
||||||
import sys, os, argparse, uuid
|
import sys, os, argparse, uuid
|
||||||
@@ -155,7 +155,7 @@ def main():
|
|||||||
\t\t\t<ObjectAutonumerationMode>NotAutoFree</ObjectAutonumerationMode>
|
\t\t\t<ObjectAutonumerationMode>NotAutoFree</ObjectAutonumerationMode>
|
||||||
\t\t\t<ModalityUseMode>DontUse</ModalityUseMode>
|
\t\t\t<ModalityUseMode>DontUse</ModalityUseMode>
|
||||||
\t\t\t<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode>
|
\t\t\t<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode>
|
||||||
\t\t\t<InterfaceCompatibilityMode>Taxi</InterfaceCompatibilityMode>
|
\t\t\t<InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode>
|
||||||
\t\t\t<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode>
|
\t\t\t<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode>
|
||||||
\t\t\t<CompatibilityMode>{compat}</CompatibilityMode>
|
\t\t\t<CompatibilityMode>{compat}</CompatibilityMode>
|
||||||
\t\t\t<DefaultConstantsForm/>
|
\t\t\t<DefaultConstantsForm/>
|
||||||
@@ -184,20 +184,50 @@ def main():
|
|||||||
\t</Language>
|
\t</Language>
|
||||||
</MetaDataObject>'''
|
</MetaDataObject>'''
|
||||||
|
|
||||||
|
# --- Ext/ClientApplicationInterface.xml (default ERP-style panel layout) ---
|
||||||
|
# Open panel on top, Sections panel on left; Functions/Favorites/History declared
|
||||||
|
# via panelDef but not placed by default. Without this file the web client renders
|
||||||
|
# section icons without labels (icon-only mode).
|
||||||
|
open_panel_inst = new_uuid()
|
||||||
|
sections_panel_inst = new_uuid()
|
||||||
|
cai_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ClientApplicationInterface xmlns="http://v8.1c.ru/8.2/managed-application/core" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="InterfaceLayouter">
|
||||||
|
\t<top>
|
||||||
|
\t\t<panel id="{open_panel_inst}">
|
||||||
|
\t\t\t<uuid>cbab57f2-a0f3-4f0a-89ea-4cb19570ab75</uuid>
|
||||||
|
\t\t</panel>
|
||||||
|
\t</top>
|
||||||
|
\t<left>
|
||||||
|
\t\t<panel id="{sections_panel_inst}">
|
||||||
|
\t\t\t<uuid>b553047f-c9aa-4157-978d-448ecad24248</uuid>
|
||||||
|
\t\t</panel>
|
||||||
|
\t</left>
|
||||||
|
\t<panelDef id="b553047f-c9aa-4157-978d-448ecad24248"/>
|
||||||
|
\t<panelDef id="13322b22-3960-4d68-93a6-fe2dd7f28ca3"/>
|
||||||
|
\t<panelDef id="c933ac92-92cd-459d-81cc-e0c8a83ced99"/>
|
||||||
|
\t<panelDef id="cbab57f2-a0f3-4f0a-89ea-4cb19570ab75"/>
|
||||||
|
\t<panelDef id="b2735bd3-d822-4430-ba59-c9e869693b24"/>
|
||||||
|
</ClientApplicationInterface>'''
|
||||||
|
|
||||||
# --- Create directories ---
|
# --- Create directories ---
|
||||||
os.makedirs(output_dir, exist_ok=True)
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
lang_dir = os.path.join(output_dir, "Languages")
|
lang_dir = os.path.join(output_dir, "Languages")
|
||||||
os.makedirs(lang_dir, exist_ok=True)
|
os.makedirs(lang_dir, exist_ok=True)
|
||||||
|
ext_dir = os.path.join(output_dir, "Ext")
|
||||||
|
os.makedirs(ext_dir, exist_ok=True)
|
||||||
|
|
||||||
# --- Write files ---
|
# --- Write files ---
|
||||||
write_utf8_bom(cfg_file, cfg_xml)
|
write_utf8_bom(cfg_file, cfg_xml)
|
||||||
lang_file = os.path.join(lang_dir, "Русский.xml")
|
lang_file = os.path.join(lang_dir, "Русский.xml")
|
||||||
write_utf8_bom(lang_file, lang_xml)
|
write_utf8_bom(lang_file, lang_xml)
|
||||||
|
cai_file = os.path.join(ext_dir, "ClientApplicationInterface.xml")
|
||||||
|
write_utf8_bom(cai_file, cai_xml)
|
||||||
|
|
||||||
print(f"[OK] Создана конфигурация: {name}")
|
print(f"[OK] Создана конфигурация: {name}")
|
||||||
print(f" Каталог: {output_dir}")
|
print(f" Каталог: {output_dir}")
|
||||||
print(f" Configuration.xml: {cfg_file}")
|
print(f" Configuration.xml: {cfg_file}")
|
||||||
print(f" Languages: {lang_file}")
|
print(f" Languages: {lang_file}")
|
||||||
|
print(f" Ext/CAI: {cai_file}")
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
name: cf-validate
|
||||||
|
description: Валидация конфигурации 1С. Используй после создания или модификации конфигурации для проверки корректности
|
||||||
|
argument-hint: <ConfigPath> [-Detailed] [-MaxErrors 30]
|
||||||
|
allowed-tools:
|
||||||
|
- Bash
|
||||||
|
- Read
|
||||||
|
- Glob
|
||||||
|
---
|
||||||
|
|
||||||
|
# /cf-validate — валидация конфигурации 1С
|
||||||
|
|
||||||
|
Проверяет Configuration.xml на структурные ошибки: XML well-formedness, InternalInfo, свойства, enum-значения, ChildObjects, DefaultLanguage, файлы языков, каталоги объектов.
|
||||||
|
|
||||||
|
## Параметры
|
||||||
|
|
||||||
|
| Параметр | Обяз. | Умолч. | Описание |
|
||||||
|
|------------|:-----:|---------|-------------------------------------------------|
|
||||||
|
| ConfigPath | да | — | Путь к Configuration.xml или каталогу выгрузки |
|
||||||
|
| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) |
|
||||||
|
| MaxErrors | нет | 30 | Остановиться после N ошибок |
|
||||||
|
| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) |
|
||||||
|
|
||||||
|
## Команда
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python ".opencode/skills/cf-validate/scripts/cf-validate.py" -ConfigPath "upload/cfempty"
|
||||||
|
python ".opencode/skills/cf-validate/scripts/cf-validate.py" -ConfigPath "upload/cfempty/Configuration.xml"
|
||||||
|
```
|
||||||
+73
-6
@@ -1,7 +1,8 @@
|
|||||||
# cf-validate v1.1 — Validate 1C configuration root structure
|
# cf-validate v1.3 — Validate 1C configuration root structure
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
|
[Alias('Path')]
|
||||||
[string]$ConfigPath,
|
[string]$ConfigPath,
|
||||||
|
|
||||||
[switch]$Detailed,
|
[switch]$Detailed,
|
||||||
@@ -145,17 +146,17 @@ $childTypeDirMap = @{
|
|||||||
|
|
||||||
# Valid enum values for Configuration properties
|
# Valid enum values for Configuration properties
|
||||||
$validEnumValues = @{
|
$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")
|
"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","Version8_5_1")
|
||||||
"DefaultRunMode" = @("ManagedApplication","OrdinaryApplication","Auto")
|
"DefaultRunMode" = @("ManagedApplication","OrdinaryApplication","Auto")
|
||||||
"ScriptVariant" = @("Russian","English")
|
"ScriptVariant" = @("Russian","English")
|
||||||
"DataLockControlMode" = @("Automatic","Managed","AutomaticAndManaged")
|
"DataLockControlMode" = @("Automatic","Managed","AutomaticAndManaged")
|
||||||
"ObjectAutonumerationMode" = @("NotAutoFree","AutoFree")
|
"ObjectAutonumerationMode" = @("NotAutoFree","AutoFree")
|
||||||
"ModalityUseMode" = @("DontUse","Use","UseWithWarnings")
|
"ModalityUseMode" = @("DontUse","Use","UseWithWarnings")
|
||||||
"SynchronousPlatformExtensionAndAddInCallUseMode" = @("DontUse","Use","UseWithWarnings")
|
"SynchronousPlatformExtensionAndAddInCallUseMode" = @("DontUse","Use","UseWithWarnings")
|
||||||
"InterfaceCompatibilityMode" = @("Taxi","TaxiEnableVersion8_2","Version8_2")
|
"InterfaceCompatibilityMode" = @("Version8_2","Version8_2EnableTaxi","Taxi","TaxiEnableVersion8_2","TaxiEnableVersion8_5","Version8_5EnableTaxi","Version8_5")
|
||||||
"DatabaseTablespacesUseMode" = @("DontUse","Use")
|
"DatabaseTablespacesUseMode" = @("DontUse","Use")
|
||||||
"MainClientApplicationWindowMode" = @("Normal","Fullscreen","Kiosk")
|
"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")
|
"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","Version8_5_1")
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- 1. Parse XML ---
|
# --- 1. Parse XML ---
|
||||||
@@ -203,8 +204,8 @@ if ($root.NamespaceURI -ne $expectedNs) {
|
|||||||
$version = $root.GetAttribute("version")
|
$version = $root.GetAttribute("version")
|
||||||
if (-not $version) {
|
if (-not $version) {
|
||||||
Report-Warn "1. Missing version attribute on MetaDataObject"
|
Report-Warn "1. Missing version attribute on MetaDataObject"
|
||||||
} elseif ($version -ne "2.17" -and $version -ne "2.20") {
|
} elseif ($version -ne "2.17" -and $version -ne "2.20" -and $version -ne "2.21") {
|
||||||
Report-Warn "1. Unusual version '$version' (expected 2.17 or 2.20)"
|
Report-Warn "1. Unusual version '$version' (expected 2.17, 2.20 or 2.21)"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Must have Configuration child
|
# Must have Configuration child
|
||||||
@@ -535,6 +536,72 @@ if ($childObjNode) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Check 9: Form references (HomePageWorkArea + Properties) ---
|
||||||
|
function Test-FormRef([string]$ref) {
|
||||||
|
if (-not $ref) { return $true }
|
||||||
|
# UUID — cannot verify without scanning all forms; skip
|
||||||
|
if ($ref -match $guidPattern) { return $true }
|
||||||
|
$parts = $ref.Split(".")
|
||||||
|
if ($parts.Count -eq 2 -and $parts[0] -eq "CommonForm") {
|
||||||
|
$p = Join-Path (Join-Path (Join-Path $configDir "CommonForms") $parts[1]) "Form.xml"
|
||||||
|
$pExt = Join-Path (Join-Path (Join-Path (Join-Path $configDir "CommonForms") $parts[1]) "Ext") "Form.xml"
|
||||||
|
return (Test-Path $p) -or (Test-Path $pExt)
|
||||||
|
}
|
||||||
|
if ($parts.Count -eq 4 -and $parts[2] -eq "Form" -and $childTypeDirMap.ContainsKey($parts[0])) {
|
||||||
|
$dir = $childTypeDirMap[$parts[0]]
|
||||||
|
$p = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $configDir $dir) $parts[1]) "Forms") $parts[3]) "Form.xml"
|
||||||
|
$pExt = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $configDir $dir) $parts[1]) "Forms") $parts[3]) "Ext") "Form.xml"
|
||||||
|
return (Test-Path $p) -or (Test-Path $pExt)
|
||||||
|
}
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
$formRefsChecked = 0
|
||||||
|
$formRefErrors = @()
|
||||||
|
|
||||||
|
# HomePageWorkArea
|
||||||
|
$hpPath = Join-Path (Join-Path $configDir "Ext") "HomePageWorkArea.xml"
|
||||||
|
if (Test-Path $hpPath) {
|
||||||
|
try {
|
||||||
|
[xml]$hpDoc = Get-Content -Path $hpPath -Encoding UTF8
|
||||||
|
$hpNs = New-Object System.Xml.XmlNamespaceManager($hpDoc.NameTable)
|
||||||
|
$hpNs.AddNamespace("hp", "http://v8.1c.ru/8.3/xcf/extrnprops")
|
||||||
|
foreach ($f in $hpDoc.DocumentElement.SelectNodes("//hp:Item/hp:Form", $hpNs)) {
|
||||||
|
$ref = $f.InnerText.Trim()
|
||||||
|
if (-not $ref) { continue }
|
||||||
|
$formRefsChecked++
|
||||||
|
if (-not (Test-FormRef $ref)) {
|
||||||
|
$formRefErrors += "HomePageWorkArea.Form '$ref' — file not found"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
$formRefErrors += "HomePageWorkArea.xml: parse error — $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Properties: DefaultXxxForm refs
|
||||||
|
if ($propsNode) {
|
||||||
|
$formProps = @("DefaultReportForm","DefaultReportVariantForm","DefaultReportSettingsForm","DefaultDynamicListSettingsForm","DefaultSearchForm","DefaultDataHistoryChangeHistoryForm","DefaultDataHistoryVersionDataForm","DefaultDataHistoryVersionDifferencesForm","DefaultCollaborationSystemUsersChoiceForm","DefaultConstantsForm")
|
||||||
|
foreach ($pn in $formProps) {
|
||||||
|
$node = $propsNode.SelectSingleNode("md:$pn", $ns)
|
||||||
|
if ($node -and $node.InnerText.Trim()) {
|
||||||
|
$ref = $node.InnerText.Trim()
|
||||||
|
$formRefsChecked++
|
||||||
|
if (-not (Test-FormRef $ref)) {
|
||||||
|
$formRefErrors += "Properties.$pn '$ref' — form not found"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($formRefsChecked -eq 0) {
|
||||||
|
Report-OK "9. Form references: none to check"
|
||||||
|
} elseif ($formRefErrors.Count -eq 0) {
|
||||||
|
Report-OK "9. Form references: $formRefsChecked verified"
|
||||||
|
} else {
|
||||||
|
foreach ($err in $formRefErrors) { Report-Error "9. $err" }
|
||||||
|
}
|
||||||
|
|
||||||
# --- Final output ---
|
# --- Final output ---
|
||||||
& $finalize
|
& $finalize
|
||||||
|
|
||||||
+64
-7
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cf-validate v1.1 — Validate 1C configuration XML structure
|
# cf-validate v1.3 — Validate 1C configuration XML structure
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
"""Validates Configuration.xml: root structure, InternalInfo, properties, ChildObjects, languages."""
|
"""Validates Configuration.xml: root structure, InternalInfo, properties, ChildObjects, languages."""
|
||||||
import sys, os, argparse, re
|
import sys, os, argparse, re
|
||||||
@@ -82,7 +82,7 @@ VALID_ENUM_VALUES = {
|
|||||||
'Version8_3_11', 'Version8_3_12', 'Version8_3_13', 'Version8_3_14', 'Version8_3_15',
|
'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_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_21', 'Version8_3_22', 'Version8_3_23', 'Version8_3_24', 'Version8_3_25',
|
||||||
'Version8_3_26', 'Version8_3_27', 'Version8_3_28',
|
'Version8_3_26', 'Version8_3_27', 'Version8_3_28', 'Version8_5_1',
|
||||||
],
|
],
|
||||||
'DefaultRunMode': ['ManagedApplication', 'OrdinaryApplication', 'Auto'],
|
'DefaultRunMode': ['ManagedApplication', 'OrdinaryApplication', 'Auto'],
|
||||||
'ScriptVariant': ['Russian', 'English'],
|
'ScriptVariant': ['Russian', 'English'],
|
||||||
@@ -90,7 +90,10 @@ VALID_ENUM_VALUES = {
|
|||||||
'ObjectAutonumerationMode': ['NotAutoFree', 'AutoFree'],
|
'ObjectAutonumerationMode': ['NotAutoFree', 'AutoFree'],
|
||||||
'ModalityUseMode': ['DontUse', 'Use', 'UseWithWarnings'],
|
'ModalityUseMode': ['DontUse', 'Use', 'UseWithWarnings'],
|
||||||
'SynchronousPlatformExtensionAndAddInCallUseMode': ['DontUse', 'Use', 'UseWithWarnings'],
|
'SynchronousPlatformExtensionAndAddInCallUseMode': ['DontUse', 'Use', 'UseWithWarnings'],
|
||||||
'InterfaceCompatibilityMode': ['Taxi', 'TaxiEnableVersion8_2', 'Version8_2'],
|
'InterfaceCompatibilityMode': [
|
||||||
|
'Version8_2', 'Version8_2EnableTaxi', 'Taxi', 'TaxiEnableVersion8_2',
|
||||||
|
'TaxiEnableVersion8_5', 'Version8_5EnableTaxi', 'Version8_5',
|
||||||
|
],
|
||||||
'DatabaseTablespacesUseMode': ['DontUse', 'Use'],
|
'DatabaseTablespacesUseMode': ['DontUse', 'Use'],
|
||||||
'MainClientApplicationWindowMode': ['Normal', 'Fullscreen', 'Kiosk'],
|
'MainClientApplicationWindowMode': ['Normal', 'Fullscreen', 'Kiosk'],
|
||||||
'CompatibilityMode': [
|
'CompatibilityMode': [
|
||||||
@@ -100,7 +103,7 @@ VALID_ENUM_VALUES = {
|
|||||||
'Version8_3_11', 'Version8_3_12', 'Version8_3_13', 'Version8_3_14', 'Version8_3_15',
|
'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_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_21', 'Version8_3_22', 'Version8_3_23', 'Version8_3_24', 'Version8_3_25',
|
||||||
'Version8_3_26', 'Version8_3_27', 'Version8_3_28',
|
'Version8_3_26', 'Version8_3_27', 'Version8_3_28', 'Version8_5_1',
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,7 +165,7 @@ def main():
|
|||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description='Validate 1C configuration XML structure', allow_abbrev=False
|
description='Validate 1C configuration XML structure', allow_abbrev=False
|
||||||
)
|
)
|
||||||
parser.add_argument('-ConfigPath', dest='ConfigPath', required=True)
|
parser.add_argument('-ConfigPath', '-Path', dest='ConfigPath', required=True)
|
||||||
parser.add_argument('-Detailed', action='store_true')
|
parser.add_argument('-Detailed', action='store_true')
|
||||||
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
|
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
|
||||||
parser.add_argument('-OutFile', dest='OutFile', default='')
|
parser.add_argument('-OutFile', dest='OutFile', default='')
|
||||||
@@ -228,8 +231,8 @@ def main():
|
|||||||
version = root.get('version', '')
|
version = root.get('version', '')
|
||||||
if not version:
|
if not version:
|
||||||
r.warn('1. Missing version attribute on MetaDataObject')
|
r.warn('1. Missing version attribute on MetaDataObject')
|
||||||
elif version not in ('2.17', '2.20'):
|
elif version not in ('2.17', '2.20', '2.21'):
|
||||||
r.warn(f"1. Unusual version '{version}' (expected 2.17 or 2.20)")
|
r.warn(f"1. Unusual version '{version}' (expected 2.17, 2.20 or 2.21)")
|
||||||
|
|
||||||
# Must have Configuration child
|
# Must have Configuration child
|
||||||
cfg_node = None
|
cfg_node = None
|
||||||
@@ -534,6 +537,60 @@ def main():
|
|||||||
else:
|
else:
|
||||||
pass # no ChildObjects
|
pass # no ChildObjects
|
||||||
|
|
||||||
|
# --- Check 9: Form references (HomePageWorkArea + Properties) ---
|
||||||
|
def test_form_ref(ref):
|
||||||
|
if not ref:
|
||||||
|
return True
|
||||||
|
if GUID_PATTERN.match(ref):
|
||||||
|
return True
|
||||||
|
parts = ref.split('.')
|
||||||
|
if len(parts) == 2 and parts[0] == 'CommonForm':
|
||||||
|
p = os.path.join(config_dir, 'CommonForms', parts[1], 'Form.xml')
|
||||||
|
p_ext = os.path.join(config_dir, 'CommonForms', parts[1], 'Ext', 'Form.xml')
|
||||||
|
return os.path.isfile(p) or os.path.isfile(p_ext)
|
||||||
|
if len(parts) == 4 and parts[2] == 'Form' and parts[0] in CHILD_TYPE_DIR_MAP:
|
||||||
|
d = CHILD_TYPE_DIR_MAP[parts[0]]
|
||||||
|
p = os.path.join(config_dir, d, parts[1], 'Forms', parts[3], 'Form.xml')
|
||||||
|
p_ext = os.path.join(config_dir, d, parts[1], 'Forms', parts[3], 'Ext', 'Form.xml')
|
||||||
|
return os.path.isfile(p) or os.path.isfile(p_ext)
|
||||||
|
return False
|
||||||
|
|
||||||
|
form_refs_checked = 0
|
||||||
|
form_ref_errors = []
|
||||||
|
|
||||||
|
hp_path = os.path.join(config_dir, 'Ext', 'HomePageWorkArea.xml')
|
||||||
|
if os.path.isfile(hp_path):
|
||||||
|
try:
|
||||||
|
hp_tree = etree.parse(hp_path)
|
||||||
|
HP_NS = 'http://v8.1c.ru/8.3/xcf/extrnprops'
|
||||||
|
for f in hp_tree.getroot().iter(f'{{{HP_NS}}}Form'):
|
||||||
|
ref = (f.text or '').strip()
|
||||||
|
if not ref:
|
||||||
|
continue
|
||||||
|
form_refs_checked += 1
|
||||||
|
if not test_form_ref(ref):
|
||||||
|
form_ref_errors.append(f"HomePageWorkArea.Form '{ref}' — file not found")
|
||||||
|
except Exception as e:
|
||||||
|
form_ref_errors.append(f'HomePageWorkArea.xml: parse error — {e}')
|
||||||
|
|
||||||
|
if props_node is not None:
|
||||||
|
form_props = ['DefaultReportForm','DefaultReportVariantForm','DefaultReportSettingsForm','DefaultDynamicListSettingsForm','DefaultSearchForm','DefaultDataHistoryChangeHistoryForm','DefaultDataHistoryVersionDataForm','DefaultDataHistoryVersionDifferencesForm','DefaultCollaborationSystemUsersChoiceForm','DefaultConstantsForm']
|
||||||
|
for pn in form_props:
|
||||||
|
node = props_node.find(f'md:{pn}', NS)
|
||||||
|
if node is not None and node.text and node.text.strip():
|
||||||
|
ref = node.text.strip()
|
||||||
|
form_refs_checked += 1
|
||||||
|
if not test_form_ref(ref):
|
||||||
|
form_ref_errors.append(f"Properties.{pn} '{ref}' — form not found")
|
||||||
|
|
||||||
|
if form_refs_checked == 0:
|
||||||
|
r.ok('9. Form references: none to check')
|
||||||
|
elif not form_ref_errors:
|
||||||
|
r.ok(f'9. Form references: {form_refs_checked} verified')
|
||||||
|
else:
|
||||||
|
for err in form_ref_errors:
|
||||||
|
r.error(f'9. {err}')
|
||||||
|
|
||||||
# --- Final output ---
|
# --- Final output ---
|
||||||
r.finalize(out_file)
|
r.finalize(out_file)
|
||||||
sys.exit(1 if r.errors > 0 else 0)
|
sys.exit(1 if r.errors > 0 else 0)
|
||||||
@@ -31,7 +31,7 @@ allowed-tools:
|
|||||||
| `ExtensionPath` | Путь к каталогу расширения (обязат.) |
|
| `ExtensionPath` | Путь к каталогу расширения (обязат.) |
|
||||||
| `ConfigPath` | Путь к конфигурации-источнику (обязат.) |
|
| `ConfigPath` | Путь к конфигурации-источнику (обязат.) |
|
||||||
| `Object` | Что заимствовать (обязат.), batch через `;;` |
|
| `Object` | Что заимствовать (обязат.), batch через `;;` |
|
||||||
| `BorrowMainAttribute` | Используй при добавлении нового реквизита на заимствованную форму. `Form` (по умолч.) — реквизиты с формы, `All` — все реквизиты объекта |
|
| `BorrowMainAttribute` | Заимствовать основной реквизит формы. Без параметра — не заимствует. `Form` — реквизиты, используемые на форме. `All` — все реквизиты объекта. Требует форму в -Object |
|
||||||
|
|
||||||
## Формат -Object
|
## Формат -Object
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File .claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты"
|
python ".opencode/skills/cfe-borrow/scripts/cfe-borrow.py" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
+227
-114
@@ -1,4 +1,4 @@
|
|||||||
# cfe-borrow v1.2 — Borrow objects from configuration into extension (CFE)
|
# cfe-borrow v1.8 — Borrow objects from configuration into extension (CFE)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)][string]$ExtensionPath,
|
[Parameter(Mandatory)][string]$ExtensionPath,
|
||||||
@@ -13,6 +13,31 @@ $ErrorActionPreference = "Stop"
|
|||||||
function Info([string]$msg) { Write-Host "[INFO] $msg" }
|
function Info([string]$msg) { Write-Host "[INFO] $msg" }
|
||||||
function Warn([string]$msg) { Write-Host "[WARN] $msg" }
|
function Warn([string]$msg) { Write-Host "[WARN] $msg" }
|
||||||
|
|
||||||
|
# Form data-binding tags (value = attribute path). A binding survives only if its root
|
||||||
|
# attribute is borrowed into the form's <Attributes>; otherwise it must be stripped or the
|
||||||
|
# platform rejects the form with "Неверный путь к данным" on load.
|
||||||
|
$script:formBindingDataTags = @('DataPath','TitleDataPath','FooterDataPath','HeaderDataPath','MultipleValueDataPath','MultipleValuePresentDataPath')
|
||||||
|
# Picture-path binding tags (value = picture index path, never a data attribute) — always stripped in the skeleton.
|
||||||
|
$script:formBindingPictureTags = @('RowPictureDataPath','MultipleValuePictureDataPath')
|
||||||
|
|
||||||
|
# Strip data-binding tags whose root attribute isn't borrowed.
|
||||||
|
# $keepObjekt=$true (BorrowMainAttribute): keep Объект.* data bindings, strip the rest.
|
||||||
|
# $keepObjekt=$false (default skeleton): strip all bindings. Picture-path tags are always stripped.
|
||||||
|
function Strip-FormBindings {
|
||||||
|
param([string]$xml, [bool]$keepObjekt)
|
||||||
|
foreach ($tag in $script:formBindingDataTags) {
|
||||||
|
if ($keepObjekt) {
|
||||||
|
$xml = [regex]::Replace($xml, "\s*<$tag>(?!Объект\.)[^<]*</$tag>", '')
|
||||||
|
} else {
|
||||||
|
$xml = [regex]::Replace($xml, "\s*<$tag>[^<]*</$tag>", '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($tag in $script:formBindingPictureTags) {
|
||||||
|
$xml = [regex]::Replace($xml, "\s*<$tag>[^<]*</$tag>", '')
|
||||||
|
}
|
||||||
|
return $xml
|
||||||
|
}
|
||||||
|
|
||||||
# --- 1. Resolve paths ---
|
# --- 1. Resolve paths ---
|
||||||
if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) {
|
if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) {
|
||||||
$ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath
|
$ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath
|
||||||
@@ -316,6 +341,25 @@ function Expand-SelfClosingElement($container, $parentIndent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- 7b. Detect format version ---
|
||||||
|
|
||||||
|
function Detect-FormatVersion([string]$dir) {
|
||||||
|
$d = $dir
|
||||||
|
while ($d) {
|
||||||
|
$cfgPath = Join-Path $d "Configuration.xml"
|
||||||
|
if (Test-Path $cfgPath) {
|
||||||
|
$head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length))
|
||||||
|
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
||||||
|
}
|
||||||
|
$parent = Split-Path $d -Parent
|
||||||
|
if ($parent -eq $d) { break }
|
||||||
|
$d = $parent
|
||||||
|
}
|
||||||
|
return "2.17"
|
||||||
|
}
|
||||||
|
|
||||||
|
$script:formatVersion = Detect-FormatVersion $extDir
|
||||||
|
|
||||||
# --- 8. Namespaces declaration for object XML ---
|
# --- 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"'
|
$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"'
|
||||||
|
|
||||||
@@ -400,6 +444,14 @@ function Read-SourceObject {
|
|||||||
$srcProps[$propName] = $propNode.InnerText.Trim()
|
$srcProps[$propName] = $propNode.InnerText.Trim()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
# DefinedType: carry the <Type> definition. A type alias is meaningless as a bare shell —
|
||||||
|
# the platform needs its underlying type (e.g. to know a column is a summable Number for totals).
|
||||||
|
if ($typeName -eq "DefinedType") {
|
||||||
|
$typeNode = $propsNode.SelectSingleNode("md:Type", $srcNs)
|
||||||
|
if ($typeNode) {
|
||||||
|
$srcProps["__TypeXml"] = [regex]::Replace($typeNode.OuterXml, '\s+xmlns(?::\w+)?="[^"]*"', '')
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return @{
|
return @{
|
||||||
@@ -462,11 +514,26 @@ function Borrow-Form {
|
|||||||
}
|
}
|
||||||
$srcFormContent = [System.IO.File]::ReadAllText($srcFormXmlPath, $enc)
|
$srcFormContent = [System.IO.File]::ReadAllText($srcFormXmlPath, $enc)
|
||||||
|
|
||||||
# 3. Generate form metadata XML (ФормаЭлемента.xml)
|
# 3. Generate form metadata XML (ФормаЭлемента.xml).
|
||||||
$newFormUuid = [guid]::NewGuid().ToString()
|
# If the wrapper was already borrowed, reuse its uuid so re-borrow is idempotent
|
||||||
|
# (regenerating it would churn the form's identity on every rerun).
|
||||||
|
$formMetaFileExisting = Join-Path (Join-Path (Join-Path (Join-Path $extDir $dirName) $objName) "Forms") "${formName}.xml"
|
||||||
|
$newFormUuid = ""
|
||||||
|
if (Test-Path $formMetaFileExisting) {
|
||||||
|
try {
|
||||||
|
$existingDoc = New-Object System.Xml.XmlDocument
|
||||||
|
$existingDoc.Load($formMetaFileExisting)
|
||||||
|
$existingFormNode = $existingDoc.DocumentElement.SelectSingleNode("*[local-name()='Form']")
|
||||||
|
if ($existingFormNode) {
|
||||||
|
$existingUuid = $existingFormNode.GetAttribute("uuid")
|
||||||
|
if ($existingUuid) { $newFormUuid = $existingUuid }
|
||||||
|
}
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
if (-not $newFormUuid) { $newFormUuid = [guid]::NewGuid().ToString() }
|
||||||
$formMetaSb = New-Object System.Text.StringBuilder
|
$formMetaSb = New-Object System.Text.StringBuilder
|
||||||
$formMetaSb.AppendLine("<?xml version=`"1.0`" encoding=`"UTF-8`"?>") | Out-Null
|
$formMetaSb.AppendLine("<?xml version=`"1.0`" encoding=`"UTF-8`"?>") | Out-Null
|
||||||
$formMetaSb.AppendLine("<MetaDataObject $($script:xmlnsDecl) version=`"2.17`">") | Out-Null
|
$formMetaSb.AppendLine("<MetaDataObject $($script:xmlnsDecl) version=`"$($script:formatVersion)`">") | Out-Null
|
||||||
$formMetaSb.AppendLine("`t<Form uuid=`"${newFormUuid}`">") | Out-Null
|
$formMetaSb.AppendLine("`t<Form uuid=`"${newFormUuid}`">") | Out-Null
|
||||||
$formMetaSb.AppendLine("`t`t<InternalInfo/>") | Out-Null
|
$formMetaSb.AppendLine("`t`t<InternalInfo/>") | Out-Null
|
||||||
$formMetaSb.AppendLine("`t`t<Properties>") | Out-Null
|
$formMetaSb.AppendLine("`t`t<Properties>") | Out-Null
|
||||||
@@ -497,8 +564,10 @@ function Borrow-Form {
|
|||||||
$srcFormDoc.Load($srcFormXmlPath)
|
$srcFormDoc.Load($srcFormXmlPath)
|
||||||
$srcFormEl = $srcFormDoc.DocumentElement
|
$srcFormEl = $srcFormDoc.DocumentElement
|
||||||
|
|
||||||
$formVersion = $srcFormEl.GetAttribute("version")
|
# Borrowed form must use the extension's format version (not the source form's), so the whole
|
||||||
if (-not $formVersion) { $formVersion = "2.17" }
|
# extension stays uniform — otherwise the platform rejects the import on a version mismatch
|
||||||
|
# (e.g. a 2.13 form inside a 2.17 extension). The platform itself upgrades the form to the root version.
|
||||||
|
$formVersion = $script:formatVersion
|
||||||
|
|
||||||
# Find direct children: form properties, AutoCommandBar, ChildItems
|
# Find direct children: form properties, AutoCommandBar, ChildItems
|
||||||
$srcAutoCmd = $null
|
$srcAutoCmd = $null
|
||||||
@@ -533,13 +602,8 @@ function Borrow-Form {
|
|||||||
$autoCmdXml = $autoCmdXml -replace '<Autofill>true</Autofill>', '<Autofill>false</Autofill>'
|
$autoCmdXml = $autoCmdXml -replace '<Autofill>true</Autofill>', '<Autofill>false</Autofill>'
|
||||||
# Strip ExcludedCommand (references to standard commands invalid in extension)
|
# Strip ExcludedCommand (references to standard commands invalid in extension)
|
||||||
$autoCmdXml = [regex]::Replace($autoCmdXml, '\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '')
|
$autoCmdXml = [regex]::Replace($autoCmdXml, '\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '')
|
||||||
# Strip DataPath in AutoCommandBar buttons
|
# Strip data-binding tags whose root attribute isn't borrowed
|
||||||
if ($BorrowMainAttr) {
|
$autoCmdXml = Strip-FormBindings $autoCmdXml ([bool]$BorrowMainAttr)
|
||||||
# Keep only Объект.* DataPaths
|
|
||||||
$autoCmdXml = [regex]::Replace($autoCmdXml, '\s*<DataPath>(?!Объект\.)[^<]*</DataPath>', '')
|
|
||||||
} else {
|
|
||||||
$autoCmdXml = [regex]::Replace($autoCmdXml, '\s*<DataPath>[^<]*</DataPath>', '')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ChildItems: copy full tree, clean up base-config references
|
# ChildItems: copy full tree, clean up base-config references
|
||||||
@@ -549,17 +613,9 @@ function Borrow-Form {
|
|||||||
$childItemsXml = [regex]::Replace($childItemsXml, $nsStripPattern, '')
|
$childItemsXml = [regex]::Replace($childItemsXml, $nsStripPattern, '')
|
||||||
# Replace all CommandName values with 0
|
# Replace all CommandName values with 0
|
||||||
$childItemsXml = [regex]::Replace($childItemsXml, '<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>')
|
$childItemsXml = [regex]::Replace($childItemsXml, '<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>')
|
||||||
# Strip DataPath, TitleDataPath, RowPictureDataPath
|
# Strip data-binding tags whose root attribute isn't borrowed
|
||||||
if ($BorrowMainAttr) {
|
# (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*/RowPicture*)
|
||||||
# Keep only Объект.* DataPaths — strip form-attribute DataPaths (not borrowed)
|
$childItemsXml = Strip-FormBindings $childItemsXml ([bool]$BorrowMainAttr)
|
||||||
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<DataPath>(?!Объект\.)[^<]*</DataPath>', '')
|
|
||||||
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<TitleDataPath>(?!Объект\.)[^<]*</TitleDataPath>', '')
|
|
||||||
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<RowPictureDataPath>[^<]*</RowPictureDataPath>', '')
|
|
||||||
} else {
|
|
||||||
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<DataPath>[^<]*</DataPath>', '')
|
|
||||||
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<TitleDataPath>[^<]*</TitleDataPath>', '')
|
|
||||||
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<RowPictureDataPath>[^<]*</RowPictureDataPath>', '')
|
|
||||||
}
|
|
||||||
# Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension)
|
# Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension)
|
||||||
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '')
|
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '')
|
||||||
# Strip TypeLink blocks with human-readable DataPath (Items.XXX — can't convert to UUID)
|
# Strip TypeLink blocks with human-readable DataPath (Items.XXX — can't convert to UUID)
|
||||||
@@ -836,14 +892,19 @@ function Borrow-Form {
|
|||||||
[System.IO.File]::WriteAllText($formXmlFile, $formXmlSb.ToString(), $enc)
|
[System.IO.File]::WriteAllText($formXmlFile, $formXmlSb.ToString(), $enc)
|
||||||
Info " Created: $formXmlFile"
|
Info " Created: $formXmlFile"
|
||||||
|
|
||||||
# 6. Create empty Module.bsl
|
# 6. Create empty Module.bsl — but NEVER overwrite an existing one (re-borrow must
|
||||||
|
# not clobber user code added to the form module).
|
||||||
$moduleDir = Join-Path $formXmlDir "Form"
|
$moduleDir = Join-Path $formXmlDir "Form"
|
||||||
if (-not (Test-Path $moduleDir)) {
|
if (-not (Test-Path $moduleDir)) {
|
||||||
New-Item -ItemType Directory -Path $moduleDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $moduleDir -Force | Out-Null
|
||||||
}
|
}
|
||||||
$moduleBslFile = Join-Path $moduleDir "Module.bsl"
|
$moduleBslFile = Join-Path $moduleDir "Module.bsl"
|
||||||
[System.IO.File]::WriteAllText($moduleBslFile, "", $enc)
|
if (Test-Path $moduleBslFile) {
|
||||||
Info " Created: $moduleBslFile"
|
Info " Preserved existing Module.bsl"
|
||||||
|
} else {
|
||||||
|
[System.IO.File]::WriteAllText($moduleBslFile, "", $enc)
|
||||||
|
Info " Created: $moduleBslFile"
|
||||||
|
}
|
||||||
|
|
||||||
# 7. Register form in parent object ChildObjects
|
# 7. Register form in parent object ChildObjects
|
||||||
Register-FormInObject $typeName $objName $formName
|
Register-FormInObject $typeName $objName $formName
|
||||||
@@ -991,8 +1052,29 @@ function Collect-FormDataPaths {
|
|||||||
$firstLevel = @{}
|
$firstLevel = @{}
|
||||||
$deepPaths = @()
|
$deepPaths = @()
|
||||||
|
|
||||||
$matches2 = [regex]::Matches($content, '<DataPath>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</DataPath>')
|
# Scan every data-binding tag (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*)
|
||||||
foreach ($m in $matches2) {
|
# for Объект.* references — picture-path tags carry picture indices, not data attributes.
|
||||||
|
foreach ($tag in $script:formBindingDataTags) {
|
||||||
|
$bms = [regex]::Matches($content, "<$tag>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</$tag>")
|
||||||
|
foreach ($m in $bms) {
|
||||||
|
$path = $m.Groups[1].Value
|
||||||
|
$segments = $path.Split(".")
|
||||||
|
$seg0 = $segments[0]
|
||||||
|
if ($script:standardFields -contains $seg0) { continue }
|
||||||
|
$firstLevel[$seg0] = $true
|
||||||
|
if ($segments.Count -ge 2) {
|
||||||
|
$seg1 = $segments[1]
|
||||||
|
if ($script:standardFields -contains $seg1) { continue }
|
||||||
|
$seg2 = if ($segments.Count -ge 3) { $segments[2] } else { $null }
|
||||||
|
$deepPaths += @{ ObjectAttr = $seg0; SubAttr = $seg1; SubSubAttr = $seg2 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Also scan <Field>Объект.X</Field> — object attributes referenced by filter/conditional-appearance
|
||||||
|
# fields (and dynamic lists), not via a *DataPath binding (e.g. УдалитьЮрФизЛицо). Designer borrows these too.
|
||||||
|
$fieldMatches = [regex]::Matches($content, "<Field>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</Field>")
|
||||||
|
foreach ($m in $fieldMatches) {
|
||||||
$path = $m.Groups[1].Value
|
$path = $m.Groups[1].Value
|
||||||
$segments = $path.Split(".")
|
$segments = $path.Split(".")
|
||||||
$seg0 = $segments[0]
|
$seg0 = $segments[0]
|
||||||
@@ -1005,21 +1087,11 @@ function Collect-FormDataPaths {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Also collect from TitleDataPath
|
|
||||||
$matches3 = [regex]::Matches($content, '<TitleDataPath>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</TitleDataPath>')
|
|
||||||
foreach ($m in $matches3) {
|
|
||||||
$path = $m.Groups[1].Value
|
|
||||||
$segments = $path.Split(".")
|
|
||||||
$seg0 = $segments[0]
|
|
||||||
if ($script:standardFields -contains $seg0) { continue }
|
|
||||||
$firstLevel[$seg0] = $true
|
|
||||||
}
|
|
||||||
|
|
||||||
# Deduplicate deep paths
|
# Deduplicate deep paths
|
||||||
$seen = @{}
|
$seen = @{}
|
||||||
$uniqueDeep = @()
|
$uniqueDeep = @()
|
||||||
foreach ($dp in $deepPaths) {
|
foreach ($dp in $deepPaths) {
|
||||||
$key = "$($dp.ObjectAttr).$($dp.SubAttr)"
|
$key = "$($dp.ObjectAttr).$($dp.SubAttr).$($dp.SubSubAttr)"
|
||||||
if (-not $seen.ContainsKey($key)) {
|
if (-not $seen.ContainsKey($key)) {
|
||||||
$seen[$key] = $true
|
$seen[$key] = $true
|
||||||
$uniqueDeep += $dp
|
$uniqueDeep += $dp
|
||||||
@@ -1123,7 +1195,8 @@ function Resolve-SourceAttributes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Extract extra Properties for main object enrichment (Hierarchical, CodeLength, etc.)
|
# Extract extra Properties for main object enrichment (Hierarchical, CodeLength, etc.)
|
||||||
$extraProps = @{}
|
# Ordered so PS emits the same property order as the Python port (dict preserves insertion order).
|
||||||
|
$extraProps = [ordered]@{}
|
||||||
$propsNode = $srcEl.SelectSingleNode("md:Properties", $srcNs)
|
$propsNode = $srcEl.SelectSingleNode("md:Properties", $srcNs)
|
||||||
if ($propsNode) {
|
if ($propsNode) {
|
||||||
$propsToExtract = @("Hierarchical","FoldersOnTop","CodeLength","DescriptionLength","CodeType","CodeAllowedLength",
|
$propsToExtract = @("Hierarchical","FoldersOnTop","CodeLength","DescriptionLength","CodeType","CodeAllowedLength",
|
||||||
@@ -1356,28 +1429,45 @@ function Borrow-MainAttribute {
|
|||||||
# Step 3: Build the adopted content and insert into main object XML
|
# Step 3: Build the adopted content and insert into main object XML
|
||||||
$objFile = Join-Path (Join-Path $extDir $dirName) "${objName}.xml"
|
$objFile = Join-Path (Join-Path $extDir $dirName) "${objName}.xml"
|
||||||
|
|
||||||
|
# Read existing object XML (needed for dedup + enrichment)
|
||||||
|
$objContent = [System.IO.File]::ReadAllText($objFile, (New-Object System.Text.UTF8Encoding($true)))
|
||||||
|
|
||||||
|
# Dedup: skip attributes/TS already present in object's ChildObjects (idempotent re-borrow)
|
||||||
|
$existingChildNames = @{}
|
||||||
|
if ($objContent -match '(?s)<ChildObjects>(.*?)</ChildObjects>') {
|
||||||
|
foreach ($nm in [regex]::Matches($Matches[1], '<Name>(\w+)</Name>')) {
|
||||||
|
$existingChildNames[$nm.Groups[1].Value] = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$insertAttrs = @($srcAttrs | Where-Object { -not $existingChildNames.ContainsKey($_.Name) })
|
||||||
|
$insertTS = @($srcTS | Where-Object { -not $existingChildNames.ContainsKey($_.Name) })
|
||||||
|
|
||||||
# Generate full object XML with attributes and TS
|
# Generate full object XML with attributes and TS
|
||||||
$contentSb = New-Object System.Text.StringBuilder
|
$contentSb = New-Object System.Text.StringBuilder
|
||||||
foreach ($attr in $srcAttrs) {
|
foreach ($attr in $insertAttrs) {
|
||||||
$attrXml = Build-AdoptedAttributeXml $attr.Name $attr.Uuid $attr.TypeXml "`t`t`t"
|
$attrXml = Build-AdoptedAttributeXml $attr.Name $attr.Uuid $attr.TypeXml "`t`t`t"
|
||||||
$contentSb.AppendLine($attrXml) | Out-Null
|
$contentSb.AppendLine($attrXml) | Out-Null
|
||||||
}
|
}
|
||||||
foreach ($ts in $srcTS) {
|
foreach ($ts in $insertTS) {
|
||||||
$tsXml = Build-AdoptedTabularSectionXml $ts.Name $ts.Uuid $ts.GeneratedTypes $ts.Attributes "`t`t`t"
|
$tsXml = Build-AdoptedTabularSectionXml $ts.Name $ts.Uuid $ts.GeneratedTypes $ts.Attributes "`t`t`t"
|
||||||
$contentSb.AppendLine($tsXml) | Out-Null
|
$contentSb.AppendLine($tsXml) | Out-Null
|
||||||
}
|
}
|
||||||
$adoptedContent = $contentSb.ToString().TrimEnd()
|
$adoptedContent = $contentSb.ToString().TrimEnd()
|
||||||
|
|
||||||
# Read existing object XML and inject
|
# Inject extra properties into the object's OWN Properties only — idempotent and anchored to the
|
||||||
$objContent = [System.IO.File]::ReadAllText($objFile, (New-Object System.Text.UTF8Encoding($true)))
|
# first ExtendedConfigurationObject (the object's). On re-borrow, adopted attributes each have their
|
||||||
|
# own ExtendedConfigurationObject; a global replace would push object props inside every <Attribute>.
|
||||||
# Inject extra properties after ExtendedConfigurationObject
|
|
||||||
if ($extraProps.Count -gt 0) {
|
if ($extraProps.Count -gt 0) {
|
||||||
|
$objPropsBlock = ""
|
||||||
|
if ($objContent -match '(?s)<Properties>(.*?)</Properties>') { $objPropsBlock = $Matches[1] }
|
||||||
$propsSb = New-Object System.Text.StringBuilder
|
$propsSb = New-Object System.Text.StringBuilder
|
||||||
foreach ($pName in $extraProps.Keys) {
|
foreach ($pName in $extraProps.Keys) {
|
||||||
|
if ($objPropsBlock -match "<$pName>") { continue }
|
||||||
$propsSb.Append("`r`n`t`t`t<${pName}>$($extraProps[$pName])</${pName}>") | Out-Null
|
$propsSb.Append("`r`n`t`t`t<${pName}>$($extraProps[$pName])</${pName}>") | Out-Null
|
||||||
}
|
}
|
||||||
$objContent = $objContent -replace '(</ExtendedConfigurationObject>)', "`$1$($propsSb.ToString())"
|
if ($propsSb.Length -gt 0) {
|
||||||
|
$objContent = ([regex]'</ExtendedConfigurationObject>').Replace($objContent, "</ExtendedConfigurationObject>$($propsSb.ToString())", 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Replace empty ChildObjects with adopted content
|
# Replace empty ChildObjects with adopted content
|
||||||
@@ -1435,79 +1525,46 @@ function Borrow-MainAttribute {
|
|||||||
|
|
||||||
# Step 5: Handle deep paths (Form mode only)
|
# Step 5: Handle deep paths (Form mode only)
|
||||||
if ($mode -eq "Form" -and $deepPaths.Count -gt 0) {
|
if ($mode -eq "Form" -and $deepPaths.Count -gt 0) {
|
||||||
# Filter out deep paths where ObjectAttr is a TabularSection (those are TS column refs, not deep attribute refs)
|
# Top-level ref deep paths: Объект.<Ref>.<Sub> — borrow the ref attribute's catalog with the sub-attribute
|
||||||
$realDeep = @()
|
$deepByAttr = @{}
|
||||||
foreach ($dp in $deepPaths) {
|
foreach ($dp in $deepPaths) {
|
||||||
if (-not $tsNames.ContainsKey($dp.ObjectAttr)) { $realDeep += $dp }
|
if ($tsNames.ContainsKey($dp.ObjectAttr)) { continue }
|
||||||
|
if (-not $deepByAttr.ContainsKey($dp.ObjectAttr)) { $deepByAttr[$dp.ObjectAttr] = @() }
|
||||||
|
if ($deepByAttr[$dp.ObjectAttr] -notcontains $dp.SubAttr) { $deepByAttr[$dp.ObjectAttr] += $dp.SubAttr }
|
||||||
}
|
}
|
||||||
|
if ($deepByAttr.Count -gt 0) {
|
||||||
if ($realDeep.Count -gt 0) {
|
Info " Processing $($deepByAttr.Count) deep path attribute(s)..."
|
||||||
Info " Processing $($realDeep.Count) deep path(s)..."
|
|
||||||
|
|
||||||
# Group by ObjectAttr → target catalog
|
|
||||||
$deepByAttr = @{}
|
|
||||||
foreach ($dp in $realDeep) {
|
|
||||||
if (-not $deepByAttr.ContainsKey($dp.ObjectAttr)) { $deepByAttr[$dp.ObjectAttr] = @() }
|
|
||||||
$deepByAttr[$dp.ObjectAttr] += $dp.SubAttr
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($attrName in $deepByAttr.Keys) {
|
foreach ($attrName in $deepByAttr.Keys) {
|
||||||
# Find the attribute's type to determine target catalog
|
|
||||||
$attrInfo = $srcAttrs | Where-Object { $_.Name -eq $attrName } | Select-Object -First 1
|
$attrInfo = $srcAttrs | Where-Object { $_.Name -eq $attrName } | Select-Object -First 1
|
||||||
if (-not $attrInfo) { continue }
|
if (-not $attrInfo) { continue }
|
||||||
|
|
||||||
# Extract catalog name from type: cfg:CatalogRef.XXX
|
|
||||||
$catMatch = [regex]::Match($attrInfo.TypeXml, 'cfg:(\w+)Ref\.(\w+)')
|
$catMatch = [regex]::Match($attrInfo.TypeXml, 'cfg:(\w+)Ref\.(\w+)')
|
||||||
if (-not $catMatch.Success) { continue }
|
if (-not $catMatch.Success) { continue }
|
||||||
|
Borrow-DeepTargetAttrs $catMatch.Groups[1].Value $catMatch.Groups[2].Value $deepByAttr[$attrName]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$targetTypeName = $catMatch.Groups[1].Value
|
# Tabular-section deep paths: Объект.<ТЧ>.<Колонка>.<Sub> — borrow the column's catalog with the sub-attribute
|
||||||
$targetObjName = $catMatch.Groups[2].Value
|
$tsDeepByCol = @{}
|
||||||
|
foreach ($dp in $deepPaths) {
|
||||||
# Ensure target is borrowed
|
if (-not $tsNames.ContainsKey($dp.ObjectAttr)) { continue }
|
||||||
if (-not (Test-ObjectBorrowed $targetTypeName $targetObjName)) {
|
if (-not $dp.SubSubAttr) { continue }
|
||||||
$tSrc = Read-SourceObject $targetTypeName $targetObjName
|
if ($script:standardFields -contains $dp.SubSubAttr) { continue }
|
||||||
$tBorrowedXml = Build-BorrowedObjectXml $targetTypeName $targetObjName $tSrc.Uuid $tSrc.Properties
|
$k = "$($dp.ObjectAttr)|$($dp.SubAttr)"
|
||||||
$tTargetDir = Join-Path $extDir $childTypeDirMap[$targetTypeName]
|
if (-not $tsDeepByCol.ContainsKey($k)) { $tsDeepByCol[$k] = @() }
|
||||||
if (-not (Test-Path $tTargetDir)) {
|
if ($tsDeepByCol[$k] -notcontains $dp.SubSubAttr) { $tsDeepByCol[$k] += $dp.SubSubAttr }
|
||||||
New-Item -ItemType Directory -Path $tTargetDir -Force | Out-Null
|
}
|
||||||
}
|
if ($tsDeepByCol.Count -gt 0) {
|
||||||
$tTargetFile = Join-Path $tTargetDir "${targetObjName}.xml"
|
Info " Processing $($tsDeepByCol.Count) tabular-section deep path(s)..."
|
||||||
[System.IO.File]::WriteAllText($tTargetFile, $tBorrowedXml, $encBom)
|
foreach ($k in $tsDeepByCol.Keys) {
|
||||||
Add-ToChildObjects $targetTypeName $targetObjName
|
$parts = $k.Split("|")
|
||||||
$script:borrowedFiles += $tTargetFile
|
$tsName = $parts[0]; $colName = $parts[1]
|
||||||
Info " Auto-borrowed for deep path: ${targetTypeName}.${targetObjName}"
|
$tsInfo = $srcTS | Where-Object { $_.Name -eq $tsName } | Select-Object -First 1
|
||||||
}
|
if (-not $tsInfo) { continue }
|
||||||
|
$colInfo = $tsInfo.Attributes | Where-Object { $_.Name -eq $colName } | Select-Object -First 1
|
||||||
# Resolve sub-attributes in target catalog
|
if (-not $colInfo) { continue }
|
||||||
$subNames = @{}
|
$catMatch = [regex]::Match($colInfo.TypeXml, 'cfg:(\w+)Ref\.(\w+)')
|
||||||
foreach ($sn in $deepByAttr[$attrName]) { $subNames[$sn] = $true }
|
if (-not $catMatch.Success) { continue }
|
||||||
$subResolved = Resolve-SourceAttributes $targetTypeName $targetObjName $subNames
|
Borrow-DeepTargetAttrs $catMatch.Groups[1].Value $catMatch.Groups[2].Value $tsDeepByCol[$k]
|
||||||
|
|
||||||
if ($subResolved.Attributes.Count -gt 0) {
|
|
||||||
Merge-AttributesIntoObject $targetTypeName $targetObjName $subResolved.Attributes
|
|
||||||
|
|
||||||
# Collect and borrow ref types from deep attributes
|
|
||||||
$subTypeXmls = @()
|
|
||||||
foreach ($sa in $subResolved.Attributes) { $subTypeXmls += $sa.TypeXml }
|
|
||||||
$subRefTypes = Collect-ReferenceTypes $subTypeXmls
|
|
||||||
foreach ($srt in $subRefTypes) {
|
|
||||||
if (-not $childTypeDirMap.ContainsKey($srt.TypeName)) { continue }
|
|
||||||
if (Test-ObjectBorrowed $srt.TypeName $srt.ObjName) { continue }
|
|
||||||
$sSrcFile = Join-Path (Join-Path $cfgDir $childTypeDirMap[$srt.TypeName]) "$($srt.ObjName).xml"
|
|
||||||
if (-not (Test-Path $sSrcFile)) { continue }
|
|
||||||
$sSrc = Read-SourceObject $srt.TypeName $srt.ObjName
|
|
||||||
$sBorrowedXml = Build-BorrowedObjectXml $srt.TypeName $srt.ObjName $sSrc.Uuid $sSrc.Properties
|
|
||||||
$sTargetDir = Join-Path $extDir $childTypeDirMap[$srt.TypeName]
|
|
||||||
if (-not (Test-Path $sTargetDir)) {
|
|
||||||
New-Item -ItemType Directory -Path $sTargetDir -Force | Out-Null
|
|
||||||
}
|
|
||||||
$sTargetFile = Join-Path $sTargetDir "$($srt.ObjName).xml"
|
|
||||||
[System.IO.File]::WriteAllText($sTargetFile, $sBorrowedXml, $encBom)
|
|
||||||
Add-ToChildObjects $srt.TypeName $srt.ObjName
|
|
||||||
$script:borrowedFiles += $sTargetFile
|
|
||||||
Info " Auto-borrowed (deep): $($srt.TypeName).$($srt.ObjName)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1515,6 +1572,57 @@ function Borrow-MainAttribute {
|
|||||||
Info " Main attribute borrowing complete"
|
Info " Main attribute borrowing complete"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- 11i. Helper: borrow a deep-path target catalog together with the referenced sub-attributes ---
|
||||||
|
# Used for both Объект.<Ref>.<Sub> (top-level ref attr) and Объект.<ТЧ>.<Колонка>.<Sub> (tabular-section
|
||||||
|
# column ref). Mirrors Designer: the referenced catalog is adopted WITH the sub-attributes the form shows,
|
||||||
|
# otherwise the platform rejects the deep DataPath ("Неверный путь к данным").
|
||||||
|
function Borrow-DeepTargetAttrs {
|
||||||
|
param([string]$targetTypeName, [string]$targetObjName, $subAttrNames)
|
||||||
|
|
||||||
|
$encBomLocal = New-Object System.Text.UTF8Encoding($true)
|
||||||
|
|
||||||
|
# Ensure target is borrowed (shell)
|
||||||
|
if (-not (Test-ObjectBorrowed $targetTypeName $targetObjName)) {
|
||||||
|
$tSrc = Read-SourceObject $targetTypeName $targetObjName
|
||||||
|
$tBorrowedXml = Build-BorrowedObjectXml $targetTypeName $targetObjName $tSrc.Uuid $tSrc.Properties
|
||||||
|
$tTargetDir = Join-Path $extDir $childTypeDirMap[$targetTypeName]
|
||||||
|
if (-not (Test-Path $tTargetDir)) { New-Item -ItemType Directory -Path $tTargetDir -Force | Out-Null }
|
||||||
|
$tTargetFile = Join-Path $tTargetDir "${targetObjName}.xml"
|
||||||
|
[System.IO.File]::WriteAllText($tTargetFile, $tBorrowedXml, $encBomLocal)
|
||||||
|
Add-ToChildObjects $targetTypeName $targetObjName
|
||||||
|
$script:borrowedFiles += $tTargetFile
|
||||||
|
Info " Auto-borrowed for deep path: ${targetTypeName}.${targetObjName}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Resolve sub-attributes in target catalog and merge them in
|
||||||
|
$subNames = @{}
|
||||||
|
foreach ($sn in $subAttrNames) { $subNames[$sn] = $true }
|
||||||
|
$subResolved = Resolve-SourceAttributes $targetTypeName $targetObjName $subNames
|
||||||
|
if ($subResolved.Attributes.Count -gt 0) {
|
||||||
|
Merge-AttributesIntoObject $targetTypeName $targetObjName $subResolved.Attributes
|
||||||
|
|
||||||
|
# Borrow ref types referenced by the sub-attributes
|
||||||
|
$subTypeXmls = @()
|
||||||
|
foreach ($sa in $subResolved.Attributes) { $subTypeXmls += $sa.TypeXml }
|
||||||
|
$subRefTypes = Collect-ReferenceTypes $subTypeXmls
|
||||||
|
foreach ($srt in $subRefTypes) {
|
||||||
|
if (-not $childTypeDirMap.ContainsKey($srt.TypeName)) { continue }
|
||||||
|
if (Test-ObjectBorrowed $srt.TypeName $srt.ObjName) { continue }
|
||||||
|
$sSrcFile = Join-Path (Join-Path $cfgDir $childTypeDirMap[$srt.TypeName]) "$($srt.ObjName).xml"
|
||||||
|
if (-not (Test-Path $sSrcFile)) { continue }
|
||||||
|
$sSrc = Read-SourceObject $srt.TypeName $srt.ObjName
|
||||||
|
$sBorrowedXml = Build-BorrowedObjectXml $srt.TypeName $srt.ObjName $sSrc.Uuid $sSrc.Properties
|
||||||
|
$sTargetDir = Join-Path $extDir $childTypeDirMap[$srt.TypeName]
|
||||||
|
if (-not (Test-Path $sTargetDir)) { New-Item -ItemType Directory -Path $sTargetDir -Force | Out-Null }
|
||||||
|
$sTargetFile = Join-Path $sTargetDir "$($srt.ObjName).xml"
|
||||||
|
[System.IO.File]::WriteAllText($sTargetFile, $sBorrowedXml, $encBomLocal)
|
||||||
|
Add-ToChildObjects $srt.TypeName $srt.ObjName
|
||||||
|
$script:borrowedFiles += $sTargetFile
|
||||||
|
Info " Auto-borrowed (deep): $($srt.TypeName).$($srt.ObjName)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# --- 12. Helper: build borrowed object XML ---
|
# --- 12. Helper: build borrowed object XML ---
|
||||||
function Build-BorrowedObjectXml {
|
function Build-BorrowedObjectXml {
|
||||||
param(
|
param(
|
||||||
@@ -1529,7 +1637,7 @@ function Build-BorrowedObjectXml {
|
|||||||
|
|
||||||
$sb = New-Object System.Text.StringBuilder
|
$sb = New-Object System.Text.StringBuilder
|
||||||
$sb.AppendLine("<?xml version=`"1.0`" encoding=`"UTF-8`"?>") | Out-Null
|
$sb.AppendLine("<?xml version=`"1.0`" encoding=`"UTF-8`"?>") | Out-Null
|
||||||
$sb.AppendLine("<MetaDataObject $($script:xmlnsDecl) version=`"2.17`">") | Out-Null
|
$sb.AppendLine("<MetaDataObject $($script:xmlnsDecl) version=`"$($script:formatVersion)`">") | Out-Null
|
||||||
$sb.AppendLine("`t<${typeName} uuid=`"${newUuid}`">") | Out-Null
|
$sb.AppendLine("`t<${typeName} uuid=`"${newUuid}`">") | Out-Null
|
||||||
|
|
||||||
# InternalInfo
|
# InternalInfo
|
||||||
@@ -1553,6 +1661,11 @@ function Build-BorrowedObjectXml {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# DefinedType: emit the carried <Type> definition (needed for the alias to resolve, e.g. totals)
|
||||||
|
if ($typeName -eq "DefinedType" -and $sourceProps.ContainsKey("__TypeXml")) {
|
||||||
|
$sb.AppendLine("`t`t`t$($sourceProps['__TypeXml'])") | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
$sb.AppendLine("`t`t</Properties>") | Out-Null
|
$sb.AppendLine("`t`t</Properties>") | Out-Null
|
||||||
|
|
||||||
# ChildObjects (for types that need it)
|
# ChildObjects (for types that need it)
|
||||||
+225
-116
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cfe-borrow v1.2 — Borrow objects from configuration into extension (CFE)
|
# cfe-borrow v1.8 — Borrow objects from configuration into extension (CFE)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -14,6 +14,36 @@ XR_NS = "http://v8.1c.ru/8.3/xcf/readable"
|
|||||||
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
|
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
|
||||||
V8_NS = "http://v8.1c.ru/8.1/data/core"
|
V8_NS = "http://v8.1c.ru/8.1/data/core"
|
||||||
|
|
||||||
|
# Form data-binding tags (value = attribute path). A binding survives only if its root
|
||||||
|
# attribute is borrowed into the form's <Attributes>; otherwise it must be stripped or the
|
||||||
|
# platform rejects the form with "Неверный путь к данным" on load.
|
||||||
|
FORM_BINDING_DATA_TAGS = ["DataPath", "TitleDataPath", "FooterDataPath", "HeaderDataPath", "MultipleValueDataPath", "MultipleValuePresentDataPath"]
|
||||||
|
# Picture-path binding tags (value = picture index path, never a data attribute) — always stripped in the skeleton.
|
||||||
|
FORM_BINDING_PICTURE_TAGS = ["RowPictureDataPath", "MultipleValuePictureDataPath"]
|
||||||
|
|
||||||
|
|
||||||
|
def strip_form_bindings(xml, keep_objekt):
|
||||||
|
"""Strip data-binding tags whose root attribute isn't borrowed.
|
||||||
|
keep_objekt=True (BorrowMainAttribute): keep Объект.* data bindings, strip the rest.
|
||||||
|
keep_objekt=False (default skeleton): strip all bindings. Picture-path tags are always stripped."""
|
||||||
|
for tag in FORM_BINDING_DATA_TAGS:
|
||||||
|
if keep_objekt:
|
||||||
|
xml = re.sub(rf'\s*<{tag}>(?!Объект\.)[^<]*</{tag}>', '', xml)
|
||||||
|
else:
|
||||||
|
xml = re.sub(rf'\s*<{tag}>[^<]*</{tag}>', '', xml)
|
||||||
|
for tag in FORM_BINDING_PICTURE_TAGS:
|
||||||
|
xml = re.sub(rf'\s*<{tag}>[^<]*</{tag}>', '', xml)
|
||||||
|
return xml
|
||||||
|
|
||||||
|
|
||||||
|
def decode_numeric_entities(s):
|
||||||
|
"""lxml emits numeric character refs (&#xNNNN;) for non-ASCII in some self-closed
|
||||||
|
elements where the PowerShell port writes literal characters. Normalize numeric refs
|
||||||
|
back to literal so PS↔PY output matches. Named entities (& < ...) are left intact."""
|
||||||
|
s = re.sub(r'&#x([0-9A-Fa-f]+);', lambda m: chr(int(m.group(1), 16)), s)
|
||||||
|
s = re.sub(r'&#(\d+);', lambda m: chr(int(m.group(1))), s)
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
def localname(el):
|
def localname(el):
|
||||||
return etree.QName(el.tag).localname
|
return etree.QName(el.tag).localname
|
||||||
@@ -254,6 +284,22 @@ XMLNS_DECL = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def detect_format_version(d):
|
||||||
|
while d:
|
||||||
|
cfg_path = os.path.join(d, "Configuration.xml")
|
||||||
|
if os.path.isfile(cfg_path):
|
||||||
|
with open(cfg_path, "r", encoding="utf-8-sig") as f:
|
||||||
|
head = f.read(2000)
|
||||||
|
m = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', head)
|
||||||
|
if m:
|
||||||
|
return m.group(1)
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
break
|
||||||
|
d = parent
|
||||||
|
return "2.17"
|
||||||
|
|
||||||
|
|
||||||
def get_child_indent(container):
|
def get_child_indent(container):
|
||||||
if container.text and "\n" in container.text:
|
if container.text and "\n" in container.text:
|
||||||
after_nl = container.text.rsplit("\n", 1)[-1]
|
after_nl = container.text.rsplit("\n", 1)[-1]
|
||||||
@@ -305,7 +351,9 @@ def expand_self_closing(container, parent_indent):
|
|||||||
|
|
||||||
def save_xml_bom(tree, path):
|
def save_xml_bom(tree, path):
|
||||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
||||||
xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"')
|
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
||||||
|
if not xml_bytes.endswith(b"\n"):
|
||||||
|
xml_bytes += b"\n"
|
||||||
with open(path, "wb") as f:
|
with open(path, "wb") as f:
|
||||||
f.write(b"\xef\xbb\xbf")
|
f.write(b"\xef\xbb\xbf")
|
||||||
f.write(xml_bytes)
|
f.write(xml_bytes)
|
||||||
@@ -363,6 +411,8 @@ def main():
|
|||||||
cfg_resolved = os.path.abspath(cfg_path)
|
cfg_resolved = os.path.abspath(cfg_path)
|
||||||
cfg_dir = os.path.dirname(cfg_resolved)
|
cfg_dir = os.path.dirname(cfg_resolved)
|
||||||
|
|
||||||
|
format_version = detect_format_version(ext_dir)
|
||||||
|
|
||||||
# --- 2. Load extension Configuration.xml ---
|
# --- 2. Load extension Configuration.xml ---
|
||||||
xml_parser = etree.XMLParser(remove_blank_text=False)
|
xml_parser = etree.XMLParser(remove_blank_text=False)
|
||||||
tree = etree.parse(ext_resolved, xml_parser)
|
tree = etree.parse(ext_resolved, xml_parser)
|
||||||
@@ -442,6 +492,13 @@ def main():
|
|||||||
prop_node = props_node.find(f"{{{MD_NS}}}{prop_name}")
|
prop_node = props_node.find(f"{{{MD_NS}}}{prop_name}")
|
||||||
if prop_node is not None:
|
if prop_node is not None:
|
||||||
src_props[prop_name] = (prop_node.text or "").strip()
|
src_props[prop_name] = (prop_node.text or "").strip()
|
||||||
|
# DefinedType: carry the <Type> definition. A type alias is meaningless as a bare shell —
|
||||||
|
# the platform needs its underlying type (e.g. to know a column is a summable Number for totals).
|
||||||
|
if type_name == "DefinedType":
|
||||||
|
type_node = props_node.find(f"{{{MD_NS}}}Type")
|
||||||
|
if type_node is not None:
|
||||||
|
type_xml = etree.tostring(type_node, encoding="unicode")
|
||||||
|
src_props["__TypeXml"] = re.sub(r'\s+xmlns(?::\w+)?="[^"]*"', '', type_xml)
|
||||||
|
|
||||||
return {"Uuid": src_uuid, "Properties": src_props, "Element": src_el}
|
return {"Uuid": src_uuid, "Properties": src_props, "Element": src_el}
|
||||||
|
|
||||||
@@ -499,7 +556,7 @@ def main():
|
|||||||
|
|
||||||
lines = []
|
lines = []
|
||||||
lines.append('<?xml version="1.0" encoding="UTF-8"?>')
|
lines.append('<?xml version="1.0" encoding="UTF-8"?>')
|
||||||
lines.append(f'<MetaDataObject {XMLNS_DECL} version="2.17">')
|
lines.append(f'<MetaDataObject {XMLNS_DECL} version="{format_version}">')
|
||||||
lines.append(f'\t<{type_name} uuid="{new_uuid_val}">')
|
lines.append(f'\t<{type_name} uuid="{new_uuid_val}">')
|
||||||
lines.append(internal_info_xml)
|
lines.append(internal_info_xml)
|
||||||
lines.append("\t\t<Properties>")
|
lines.append("\t\t<Properties>")
|
||||||
@@ -513,6 +570,10 @@ def main():
|
|||||||
prop_val = source_props.get(prop_name, "false")
|
prop_val = source_props.get(prop_name, "false")
|
||||||
lines.append(f"\t\t\t<{prop_name}>{prop_val}</{prop_name}>")
|
lines.append(f"\t\t\t<{prop_name}>{prop_val}</{prop_name}>")
|
||||||
|
|
||||||
|
# DefinedType: emit the carried <Type> definition (needed for the alias to resolve, e.g. totals)
|
||||||
|
if type_name == "DefinedType" and "__TypeXml" in source_props:
|
||||||
|
lines.append(f"\t\t\t{source_props['__TypeXml']}")
|
||||||
|
|
||||||
lines.append("\t\t</Properties>")
|
lines.append("\t\t</Properties>")
|
||||||
|
|
||||||
if type_name in TYPES_WITH_CHILD_OBJECTS:
|
if type_name in TYPES_WITH_CHILD_OBJECTS:
|
||||||
@@ -624,7 +685,26 @@ def main():
|
|||||||
first_level = {}
|
first_level = {}
|
||||||
deep_paths = []
|
deep_paths = []
|
||||||
|
|
||||||
for m in re.finditer(r'<DataPath>[^<]*\b\u041e\u0431\u044a\u0435\u043a\u0442\.(\w+(?:\.\w+)*)</DataPath>', content):
|
# Scan every data-binding tag (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*)
|
||||||
|
# for Объект.* references — picture-path tags carry picture indices, not data attributes.
|
||||||
|
for tag in FORM_BINDING_DATA_TAGS:
|
||||||
|
for m in re.finditer(r'<' + tag + r'>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</' + tag + r'>', content):
|
||||||
|
path = m.group(1)
|
||||||
|
segments = path.split(".")
|
||||||
|
seg0 = segments[0]
|
||||||
|
if seg0 in STANDARD_FIELDS:
|
||||||
|
continue
|
||||||
|
first_level[seg0] = True
|
||||||
|
if len(segments) >= 2:
|
||||||
|
seg1 = segments[1]
|
||||||
|
if seg1 in STANDARD_FIELDS:
|
||||||
|
continue
|
||||||
|
seg2 = segments[2] if len(segments) >= 3 else None
|
||||||
|
deep_paths.append({"ObjectAttr": seg0, "SubAttr": seg1, "SubSubAttr": seg2})
|
||||||
|
|
||||||
|
# Also scan <Field>Объект.X</Field> — object attributes referenced by filter/conditional-appearance
|
||||||
|
# fields (and dynamic lists), not via a *DataPath binding (e.g. УдалитьЮрФизЛицо). Designer borrows these too.
|
||||||
|
for m in re.finditer(r'<Field>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</Field>', content):
|
||||||
path = m.group(1)
|
path = m.group(1)
|
||||||
segments = path.split(".")
|
segments = path.split(".")
|
||||||
seg0 = segments[0]
|
seg0 = segments[0]
|
||||||
@@ -635,22 +715,14 @@ def main():
|
|||||||
seg1 = segments[1]
|
seg1 = segments[1]
|
||||||
if seg1 in STANDARD_FIELDS:
|
if seg1 in STANDARD_FIELDS:
|
||||||
continue
|
continue
|
||||||
deep_paths.append({"ObjectAttr": seg0, "SubAttr": seg1})
|
seg2 = segments[2] if len(segments) >= 3 else None
|
||||||
|
deep_paths.append({"ObjectAttr": seg0, "SubAttr": seg1, "SubSubAttr": seg2})
|
||||||
# Also collect from TitleDataPath
|
|
||||||
for m in re.finditer(r'<TitleDataPath>[^<]*\b\u041e\u0431\u044a\u0435\u043a\u0442\.(\w+(?:\.\w+)*)</TitleDataPath>', content):
|
|
||||||
path = m.group(1)
|
|
||||||
segments = path.split(".")
|
|
||||||
seg0 = segments[0]
|
|
||||||
if seg0 in STANDARD_FIELDS:
|
|
||||||
continue
|
|
||||||
first_level[seg0] = True
|
|
||||||
|
|
||||||
# Deduplicate deep paths
|
# Deduplicate deep paths
|
||||||
seen = set()
|
seen = set()
|
||||||
unique_deep = []
|
unique_deep = []
|
||||||
for dp in deep_paths:
|
for dp in deep_paths:
|
||||||
key = f"{dp['ObjectAttr']}.{dp['SubAttr']}"
|
key = f"{dp['ObjectAttr']}.{dp['SubAttr']}.{dp.get('SubSubAttr')}"
|
||||||
if key not in seen:
|
if key not in seen:
|
||||||
seen.add(key)
|
seen.add(key)
|
||||||
unique_deep.append(dp)
|
unique_deep.append(dp)
|
||||||
@@ -921,26 +993,40 @@ def main():
|
|||||||
# Step 3: Build the adopted content and insert into main object XML
|
# Step 3: Build the adopted content and insert into main object XML
|
||||||
obj_file = os.path.join(ext_dir, dir_name, f"{obj_name}.xml")
|
obj_file = os.path.join(ext_dir, dir_name, f"{obj_name}.xml")
|
||||||
|
|
||||||
# Generate full object XML with attributes and TS
|
# Read existing object XML (needed for dedup + enrichment)
|
||||||
content_parts = []
|
|
||||||
for attr in src_attrs:
|
|
||||||
attr_xml = build_adopted_attribute_xml(attr["Name"], attr["Uuid"], attr["TypeXml"], "\t\t\t")
|
|
||||||
content_parts.append(attr_xml)
|
|
||||||
for ts in src_ts:
|
|
||||||
ts_xml = build_adopted_tabular_section_xml(ts["Name"], ts["Uuid"], ts["GeneratedTypes"], ts["Attributes"], "\t\t\t")
|
|
||||||
content_parts.append(ts_xml)
|
|
||||||
adopted_content = "\n".join(content_parts).rstrip()
|
|
||||||
|
|
||||||
# Read existing object XML and inject
|
|
||||||
with open(obj_file, "r", encoding="utf-8-sig") as fh:
|
with open(obj_file, "r", encoding="utf-8-sig") as fh:
|
||||||
obj_content = fh.read()
|
obj_content = fh.read()
|
||||||
|
|
||||||
# Inject extra properties after ExtendedConfigurationObject
|
# Dedup: skip attributes/TS already present in object's ChildObjects (idempotent re-borrow)
|
||||||
|
existing_child_names = set()
|
||||||
|
m_co = re.search(r'(?s)<ChildObjects>(.*?)</ChildObjects>', obj_content)
|
||||||
|
if m_co:
|
||||||
|
for nm in re.findall(r'<Name>(\w+)</Name>', m_co.group(1)):
|
||||||
|
existing_child_names.add(nm)
|
||||||
|
insert_attrs = [a for a in src_attrs if a["Name"] not in existing_child_names]
|
||||||
|
insert_ts = [t for t in src_ts if t["Name"] not in existing_child_names]
|
||||||
|
|
||||||
|
# Generate full object XML with attributes and TS
|
||||||
|
content_parts = []
|
||||||
|
for attr in insert_attrs:
|
||||||
|
content_parts.append(build_adopted_attribute_xml(attr["Name"], attr["Uuid"], attr["TypeXml"], "\t\t\t"))
|
||||||
|
for ts in insert_ts:
|
||||||
|
content_parts.append(build_adopted_tabular_section_xml(ts["Name"], ts["Uuid"], ts["GeneratedTypes"], ts["Attributes"], "\t\t\t"))
|
||||||
|
adopted_content = "\n".join(content_parts).rstrip()
|
||||||
|
|
||||||
|
# Inject extra properties into the object's OWN Properties only — idempotent and anchored to the
|
||||||
|
# first ExtendedConfigurationObject (the object's). On re-borrow, adopted attributes each have their
|
||||||
|
# own ExtendedConfigurationObject; a global replace would push object props inside every <Attribute>.
|
||||||
if extra_props:
|
if extra_props:
|
||||||
|
m_props = re.search(r'(?s)<Properties>(.*?)</Properties>', obj_content)
|
||||||
|
obj_props_block = m_props.group(1) if m_props else ""
|
||||||
props_xml = ""
|
props_xml = ""
|
||||||
for p_name, p_val in extra_props.items():
|
for p_name, p_val in extra_props.items():
|
||||||
|
if f"<{p_name}>" in obj_props_block:
|
||||||
|
continue
|
||||||
props_xml += f"\r\n\t\t\t<{p_name}>{p_val}</{p_name}>"
|
props_xml += f"\r\n\t\t\t<{p_name}>{p_val}</{p_name}>"
|
||||||
obj_content = obj_content.replace("</ExtendedConfigurationObject>", f"</ExtendedConfigurationObject>{props_xml}")
|
if props_xml:
|
||||||
|
obj_content = obj_content.replace("</ExtendedConfigurationObject>", f"</ExtendedConfigurationObject>{props_xml}", 1)
|
||||||
|
|
||||||
# Replace empty ChildObjects with adopted content
|
# Replace empty ChildObjects with adopted content
|
||||||
if adopted_content:
|
if adopted_content:
|
||||||
@@ -992,79 +1078,93 @@ def main():
|
|||||||
|
|
||||||
# Step 5: Handle deep paths (Form mode only)
|
# Step 5: Handle deep paths (Form mode only)
|
||||||
if mode == "Form" and deep_paths:
|
if mode == "Form" and deep_paths:
|
||||||
# Filter out deep paths where ObjectAttr is a TabularSection
|
# Top-level ref deep paths: Объект.<Ref>.<Sub> — borrow the ref attribute's catalog with the sub-attribute
|
||||||
real_deep = [dp for dp in deep_paths if dp["ObjectAttr"] not in ts_names]
|
deep_by_attr = {}
|
||||||
|
for dp in deep_paths:
|
||||||
if real_deep:
|
if dp["ObjectAttr"] in ts_names:
|
||||||
info(f" Processing {len(real_deep)} deep path(s)...")
|
continue
|
||||||
|
deep_by_attr.setdefault(dp["ObjectAttr"], [])
|
||||||
# Group by ObjectAttr -> target catalog
|
if dp["SubAttr"] not in deep_by_attr[dp["ObjectAttr"]]:
|
||||||
deep_by_attr = {}
|
|
||||||
for dp in real_deep:
|
|
||||||
if dp["ObjectAttr"] not in deep_by_attr:
|
|
||||||
deep_by_attr[dp["ObjectAttr"]] = []
|
|
||||||
deep_by_attr[dp["ObjectAttr"]].append(dp["SubAttr"])
|
deep_by_attr[dp["ObjectAttr"]].append(dp["SubAttr"])
|
||||||
|
if deep_by_attr:
|
||||||
|
info(f" Processing {len(deep_by_attr)} deep path attribute(s)...")
|
||||||
for attr_name, sub_attr_names in deep_by_attr.items():
|
for attr_name, sub_attr_names in deep_by_attr.items():
|
||||||
# Find the attribute's type to determine target catalog
|
attr_info = next((a for a in src_attrs if a["Name"] == attr_name), None)
|
||||||
attr_info = None
|
|
||||||
for a in src_attrs:
|
|
||||||
if a["Name"] == attr_name:
|
|
||||||
attr_info = a
|
|
||||||
break
|
|
||||||
if not attr_info:
|
if not attr_info:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Extract catalog name from type: cfg:CatalogRef.XXX
|
|
||||||
cat_match = re.search(r'cfg:(\w+)Ref\.(\w+)', attr_info["TypeXml"])
|
cat_match = re.search(r'cfg:(\w+)Ref\.(\w+)', attr_info["TypeXml"])
|
||||||
if not cat_match:
|
if not cat_match:
|
||||||
continue
|
continue
|
||||||
|
borrow_deep_target_attrs(cat_match.group(1), cat_match.group(2), sub_attr_names)
|
||||||
|
|
||||||
target_type_name = cat_match.group(1)
|
# Tabular-section deep paths: Объект.<ТЧ>.<Колонка>.<Sub> — borrow the column's catalog with the sub-attribute
|
||||||
target_obj_name = cat_match.group(2)
|
ts_deep_by_col = {}
|
||||||
|
for dp in deep_paths:
|
||||||
# Ensure target is borrowed
|
if dp["ObjectAttr"] not in ts_names:
|
||||||
if not test_object_borrowed(target_type_name, target_obj_name):
|
continue
|
||||||
t_src = read_source_object(target_type_name, target_obj_name)
|
if not dp.get("SubSubAttr"):
|
||||||
t_borrowed_xml = build_borrowed_object_xml(target_type_name, target_obj_name, t_src["Uuid"], t_src["Properties"])
|
continue
|
||||||
t_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[target_type_name])
|
if dp["SubSubAttr"] in STANDARD_FIELDS:
|
||||||
os.makedirs(t_target_dir, exist_ok=True)
|
continue
|
||||||
t_target_file = os.path.join(t_target_dir, f"{target_obj_name}.xml")
|
k = (dp["ObjectAttr"], dp["SubAttr"])
|
||||||
save_text_bom(t_target_file, t_borrowed_xml)
|
ts_deep_by_col.setdefault(k, [])
|
||||||
add_to_child_objects(target_type_name, target_obj_name)
|
if dp["SubSubAttr"] not in ts_deep_by_col[k]:
|
||||||
borrowed_files.append(t_target_file)
|
ts_deep_by_col[k].append(dp["SubSubAttr"])
|
||||||
info(f" Auto-borrowed for deep path: {target_type_name}.{target_obj_name}")
|
if ts_deep_by_col:
|
||||||
|
info(f" Processing {len(ts_deep_by_col)} tabular-section deep path(s)...")
|
||||||
# Resolve sub-attributes in target catalog
|
for (ts_name, col_name), sub_attr_names in ts_deep_by_col.items():
|
||||||
sub_names = {sn: True for sn in sub_attr_names}
|
ts_info = next((t for t in src_ts if t["Name"] == ts_name), None)
|
||||||
sub_resolved = resolve_source_attributes(target_type_name, target_obj_name, sub_names)
|
if not ts_info:
|
||||||
|
continue
|
||||||
if sub_resolved["Attributes"]:
|
col_info = next((c for c in ts_info["Attributes"] if c["Name"] == col_name), None)
|
||||||
merge_attributes_into_object(target_type_name, target_obj_name, sub_resolved["Attributes"])
|
if not col_info:
|
||||||
|
continue
|
||||||
# Collect and borrow ref types from deep attributes
|
cat_match = re.search(r'cfg:(\w+)Ref\.(\w+)', col_info["TypeXml"])
|
||||||
sub_type_xmls = [sa["TypeXml"] for sa in sub_resolved["Attributes"]]
|
if not cat_match:
|
||||||
sub_ref_types = collect_reference_types(sub_type_xmls)
|
continue
|
||||||
for srt in sub_ref_types:
|
borrow_deep_target_attrs(cat_match.group(1), cat_match.group(2), sub_attr_names)
|
||||||
if srt["TypeName"] not in CHILD_TYPE_DIR_MAP:
|
|
||||||
continue
|
|
||||||
if test_object_borrowed(srt["TypeName"], srt["ObjName"]):
|
|
||||||
continue
|
|
||||||
s_src_file = os.path.join(cfg_dir, CHILD_TYPE_DIR_MAP[srt["TypeName"]], f"{srt['ObjName']}.xml")
|
|
||||||
if not os.path.isfile(s_src_file):
|
|
||||||
continue
|
|
||||||
s_src = read_source_object(srt["TypeName"], srt["ObjName"])
|
|
||||||
s_borrowed_xml = build_borrowed_object_xml(srt["TypeName"], srt["ObjName"], s_src["Uuid"], s_src["Properties"])
|
|
||||||
s_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[srt["TypeName"]])
|
|
||||||
os.makedirs(s_target_dir, exist_ok=True)
|
|
||||||
s_target_file = os.path.join(s_target_dir, f"{srt['ObjName']}.xml")
|
|
||||||
save_text_bom(s_target_file, s_borrowed_xml)
|
|
||||||
add_to_child_objects(srt["TypeName"], srt["ObjName"])
|
|
||||||
borrowed_files.append(s_target_file)
|
|
||||||
info(f" Auto-borrowed (deep): {srt['TypeName']}.{srt['ObjName']}")
|
|
||||||
|
|
||||||
info(" Main attribute borrowing complete")
|
info(" Main attribute borrowing complete")
|
||||||
|
|
||||||
|
def borrow_deep_target_attrs(target_type_name, target_obj_name, sub_attr_names):
|
||||||
|
# Borrow a deep-path target catalog together with the referenced sub-attributes, for both
|
||||||
|
# Объект.<Ref>.<Sub> and Объект.<ТЧ>.<Колонка>.<Sub>. Mirrors Designer: the referenced catalog
|
||||||
|
# is adopted WITH the sub-attributes the form shows, else the platform rejects the deep DataPath.
|
||||||
|
if not test_object_borrowed(target_type_name, target_obj_name):
|
||||||
|
t_src = read_source_object(target_type_name, target_obj_name)
|
||||||
|
t_borrowed_xml = build_borrowed_object_xml(target_type_name, target_obj_name, t_src["Uuid"], t_src["Properties"])
|
||||||
|
t_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[target_type_name])
|
||||||
|
os.makedirs(t_target_dir, exist_ok=True)
|
||||||
|
t_target_file = os.path.join(t_target_dir, f"{target_obj_name}.xml")
|
||||||
|
save_text_bom(t_target_file, t_borrowed_xml)
|
||||||
|
add_to_child_objects(target_type_name, target_obj_name)
|
||||||
|
borrowed_files.append(t_target_file)
|
||||||
|
info(f" Auto-borrowed for deep path: {target_type_name}.{target_obj_name}")
|
||||||
|
|
||||||
|
sub_names = {sn: True for sn in sub_attr_names}
|
||||||
|
sub_resolved = resolve_source_attributes(target_type_name, target_obj_name, sub_names)
|
||||||
|
if sub_resolved["Attributes"]:
|
||||||
|
merge_attributes_into_object(target_type_name, target_obj_name, sub_resolved["Attributes"])
|
||||||
|
sub_type_xmls = [sa["TypeXml"] for sa in sub_resolved["Attributes"]]
|
||||||
|
sub_ref_types = collect_reference_types(sub_type_xmls)
|
||||||
|
for srt in sub_ref_types:
|
||||||
|
if srt["TypeName"] not in CHILD_TYPE_DIR_MAP:
|
||||||
|
continue
|
||||||
|
if test_object_borrowed(srt["TypeName"], srt["ObjName"]):
|
||||||
|
continue
|
||||||
|
s_src_file = os.path.join(cfg_dir, CHILD_TYPE_DIR_MAP[srt["TypeName"]], f"{srt['ObjName']}.xml")
|
||||||
|
if not os.path.isfile(s_src_file):
|
||||||
|
continue
|
||||||
|
s_src = read_source_object(srt["TypeName"], srt["ObjName"])
|
||||||
|
s_borrowed_xml = build_borrowed_object_xml(srt["TypeName"], srt["ObjName"], s_src["Uuid"], s_src["Properties"])
|
||||||
|
s_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[srt["TypeName"]])
|
||||||
|
os.makedirs(s_target_dir, exist_ok=True)
|
||||||
|
s_target_file = os.path.join(s_target_dir, f"{srt['ObjName']}.xml")
|
||||||
|
save_text_bom(s_target_file, s_borrowed_xml)
|
||||||
|
add_to_child_objects(srt["TypeName"], srt["ObjName"])
|
||||||
|
borrowed_files.append(s_target_file)
|
||||||
|
info(f" Auto-borrowed (deep): {srt['TypeName']}.{srt['ObjName']}")
|
||||||
|
|
||||||
def borrow_form(type_name, obj_name, form_name, borrow_main_attr=False):
|
def borrow_form(type_name, obj_name, form_name, borrow_main_attr=False):
|
||||||
dir_name = CHILD_TYPE_DIR_MAP[type_name]
|
dir_name = CHILD_TYPE_DIR_MAP[type_name]
|
||||||
|
|
||||||
@@ -1080,11 +1180,25 @@ def main():
|
|||||||
with open(src_form_xml_path, "r", encoding="utf-8-sig") as fh:
|
with open(src_form_xml_path, "r", encoding="utf-8-sig") as fh:
|
||||||
src_form_content = fh.read()
|
src_form_content = fh.read()
|
||||||
|
|
||||||
# 3. Generate form metadata XML
|
# 3. Generate form metadata XML.
|
||||||
new_form_uuid = new_guid()
|
# If the wrapper was already borrowed, reuse its uuid so re-borrow is idempotent
|
||||||
|
# (regenerating it would churn the form's identity on every rerun).
|
||||||
|
existing_wrapper = os.path.join(ext_dir, dir_name, obj_name, "Forms", f"{form_name}.xml")
|
||||||
|
new_form_uuid = ""
|
||||||
|
if os.path.isfile(existing_wrapper):
|
||||||
|
try:
|
||||||
|
existing_root = etree.parse(existing_wrapper).getroot()
|
||||||
|
for c in existing_root:
|
||||||
|
if isinstance(c.tag, str) and localname(c) == "Form":
|
||||||
|
new_form_uuid = c.get("uuid", "") or ""
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
new_form_uuid = ""
|
||||||
|
if not new_form_uuid:
|
||||||
|
new_form_uuid = new_guid()
|
||||||
form_meta_lines = [
|
form_meta_lines = [
|
||||||
'<?xml version="1.0" encoding="UTF-8"?>',
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
||||||
f'<MetaDataObject {XMLNS_DECL} version="2.17">',
|
f'<MetaDataObject {XMLNS_DECL} version="{format_version}">',
|
||||||
f'\t<Form uuid="{new_form_uuid}">',
|
f'\t<Form uuid="{new_form_uuid}">',
|
||||||
'\t\t<InternalInfo/>',
|
'\t\t<InternalInfo/>',
|
||||||
'\t\t<Properties>',
|
'\t\t<Properties>',
|
||||||
@@ -1111,7 +1225,10 @@ def main():
|
|||||||
src_form_tree = etree.parse(src_form_xml_path, src_form_parser)
|
src_form_tree = etree.parse(src_form_xml_path, src_form_parser)
|
||||||
src_form_el = src_form_tree.getroot()
|
src_form_el = src_form_tree.getroot()
|
||||||
|
|
||||||
form_version = src_form_el.get("version", "2.17")
|
# Borrowed form uses the extension's format version (not the source form's) — keeps the
|
||||||
|
# extension uniform; otherwise the platform rejects the import on a version mismatch
|
||||||
|
# (e.g. a 2.13 form inside a 2.17 extension). The platform upgrades the form to the root version.
|
||||||
|
form_version = format_version
|
||||||
|
|
||||||
src_auto_cmd = None
|
src_auto_cmd = None
|
||||||
form_props = []
|
form_props = []
|
||||||
@@ -1129,25 +1246,21 @@ def main():
|
|||||||
continue
|
continue
|
||||||
if not reached_visual:
|
if not reached_visual:
|
||||||
# Form-level properties before AutoCommandBar (WindowOpeningMode, AutoFillCheck, etc.)
|
# Form-level properties before AutoCommandBar (WindowOpeningMode, AutoFillCheck, etc.)
|
||||||
form_props.append(etree.tostring(fc, encoding="unicode"))
|
form_props.append(decode_numeric_entities(etree.tostring(fc, encoding="unicode")))
|
||||||
|
|
||||||
ns_strip_pattern = re.compile(r'\s+xmlns(?::\w+)?="[^"]*"')
|
ns_strip_pattern = re.compile(r'\s+xmlns(?::\w+)?="[^"]*"')
|
||||||
|
|
||||||
# AutoCommandBar: keep ChildItems (buttons with CommandName->0), Autofill->false
|
# AutoCommandBar: keep ChildItems (buttons with CommandName->0), Autofill->false
|
||||||
auto_cmd_xml = ""
|
auto_cmd_xml = ""
|
||||||
if src_auto_cmd is not None:
|
if src_auto_cmd is not None:
|
||||||
auto_cmd_xml = etree.tostring(src_auto_cmd, encoding="unicode")
|
auto_cmd_xml = decode_numeric_entities(etree.tostring(src_auto_cmd, encoding="unicode"))
|
||||||
auto_cmd_xml = ns_strip_pattern.sub("", auto_cmd_xml)
|
auto_cmd_xml = ns_strip_pattern.sub("", auto_cmd_xml)
|
||||||
auto_cmd_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', auto_cmd_xml)
|
auto_cmd_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', auto_cmd_xml)
|
||||||
auto_cmd_xml = auto_cmd_xml.replace('<Autofill>true</Autofill>', '<Autofill>false</Autofill>')
|
auto_cmd_xml = auto_cmd_xml.replace('<Autofill>true</Autofill>', '<Autofill>false</Autofill>')
|
||||||
# Strip ExcludedCommand (references to standard commands invalid in extension)
|
# Strip ExcludedCommand (references to standard commands invalid in extension)
|
||||||
auto_cmd_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', auto_cmd_xml)
|
auto_cmd_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', auto_cmd_xml)
|
||||||
# Strip DataPath in AutoCommandBar buttons
|
# Strip data-binding tags whose root attribute isn't borrowed
|
||||||
if borrow_main_attr:
|
auto_cmd_xml = strip_form_bindings(auto_cmd_xml, borrow_main_attr)
|
||||||
# Keep only Объект.* DataPaths
|
|
||||||
auto_cmd_xml = re.sub(r'\s*<DataPath>(?!\u041e\u0431\u044a\u0435\u043a\u0442\.)[^<]*</DataPath>', '', auto_cmd_xml)
|
|
||||||
else:
|
|
||||||
auto_cmd_xml = re.sub(r'\s*<DataPath>[^<]*</DataPath>', '', auto_cmd_xml)
|
|
||||||
|
|
||||||
# ChildItems: copy full tree, clean up base-config references
|
# ChildItems: copy full tree, clean up base-config references
|
||||||
child_items_xml = ""
|
child_items_xml = ""
|
||||||
@@ -1158,20 +1271,12 @@ def main():
|
|||||||
break
|
break
|
||||||
|
|
||||||
if src_child_items is not None:
|
if src_child_items is not None:
|
||||||
child_items_xml = etree.tostring(src_child_items, encoding="unicode")
|
child_items_xml = decode_numeric_entities(etree.tostring(src_child_items, encoding="unicode"))
|
||||||
child_items_xml = ns_strip_pattern.sub("", child_items_xml)
|
child_items_xml = ns_strip_pattern.sub("", child_items_xml)
|
||||||
# Replace all CommandName values with 0
|
# Replace all CommandName values with 0
|
||||||
child_items_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', child_items_xml)
|
child_items_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', child_items_xml)
|
||||||
# Strip DataPath / TitleDataPath / RowPictureDataPath
|
# Strip data-binding tags whose root attribute isn't borrowed
|
||||||
if borrow_main_attr:
|
child_items_xml = strip_form_bindings(child_items_xml, borrow_main_attr)
|
||||||
# Keep only Объект.* DataPaths — strip form-attribute DataPaths (not borrowed)
|
|
||||||
child_items_xml = re.sub(r'\s*<DataPath>(?!\u041e\u0431\u044a\u0435\u043a\u0442\.)[^<]*</DataPath>', '', child_items_xml)
|
|
||||||
child_items_xml = re.sub(r'\s*<TitleDataPath>(?!\u041e\u0431\u044a\u0435\u043a\u0442\.)[^<]*</TitleDataPath>', '', child_items_xml)
|
|
||||||
child_items_xml = re.sub(r'\s*<RowPictureDataPath>[^<]*</RowPictureDataPath>', '', child_items_xml)
|
|
||||||
else:
|
|
||||||
child_items_xml = re.sub(r'\s*<DataPath>[^<]*</DataPath>', '', child_items_xml)
|
|
||||||
child_items_xml = re.sub(r'\s*<TitleDataPath>[^<]*</TitleDataPath>', '', child_items_xml)
|
|
||||||
child_items_xml = re.sub(r'\s*<RowPictureDataPath>[^<]*</RowPictureDataPath>', '', child_items_xml)
|
|
||||||
# Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension)
|
# Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension)
|
||||||
child_items_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', child_items_xml)
|
child_items_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', child_items_xml)
|
||||||
# Strip TypeLink blocks with human-readable DataPath (Items.XXX)
|
# Strip TypeLink blocks with human-readable DataPath (Items.XXX)
|
||||||
@@ -1408,12 +1513,16 @@ def main():
|
|||||||
save_text_bom(form_xml_file, "".join(parts))
|
save_text_bom(form_xml_file, "".join(parts))
|
||||||
info(f" Created: {form_xml_file}")
|
info(f" Created: {form_xml_file}")
|
||||||
|
|
||||||
# 6. Create empty Module.bsl
|
# 6. Create empty Module.bsl — but NEVER overwrite an existing one (re-borrow must
|
||||||
|
# not clobber user code added to the form module).
|
||||||
module_dir = os.path.join(form_xml_dir, "Form")
|
module_dir = os.path.join(form_xml_dir, "Form")
|
||||||
os.makedirs(module_dir, exist_ok=True)
|
os.makedirs(module_dir, exist_ok=True)
|
||||||
module_bsl_file = os.path.join(module_dir, "Module.bsl")
|
module_bsl_file = os.path.join(module_dir, "Module.bsl")
|
||||||
save_text_bom(module_bsl_file, "")
|
if os.path.isfile(module_bsl_file):
|
||||||
info(f" Created: {module_bsl_file}")
|
info(" Preserved existing Module.bsl")
|
||||||
|
else:
|
||||||
|
save_text_bom(module_bsl_file, "")
|
||||||
|
info(f" Created: {module_bsl_file}")
|
||||||
|
|
||||||
# 7. Register form in parent object ChildObjects
|
# 7. Register form in parent object ChildObjects
|
||||||
register_form_in_object(type_name, obj_name, form_name)
|
register_form_in_object(type_name, obj_name, form_name)
|
||||||
@@ -23,7 +23,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File .claude/skills/cfe-diff/scripts/cfe-diff.ps1 -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A
|
python ".opencode/skills/cfe-diff/scripts/cfe-diff.py" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A
|
||||||
```
|
```
|
||||||
|
|
||||||
## Mode A — обзор расширения
|
## Mode A — обзор расширения
|
||||||
@@ -32,19 +32,6 @@ powershell.exe -NoProfile -File .claude/skills/cfe-diff/scripts/cfe-diff.ps1 -Ex
|
|||||||
- `[BORROWED]` — заимствованный: перехватчики (`&Перед`, `&После`, `&ИзменениеИКонтроль`, `&Вместо`), собственные реквизиты/ТЧ/формы
|
- `[BORROWED]` — заимствованный: перехватчики (`&Перед`, `&После`, `&ИзменениеИКонтроль`, `&Вместо`), собственные реквизиты/ТЧ/формы
|
||||||
- `[OWN]` — собственный: количество реквизитов, ТЧ, форм
|
- `[OWN]` — собственный: количество реквизитов, ТЧ, форм
|
||||||
|
|
||||||
Пример вывода:
|
|
||||||
```
|
|
||||||
[BORROWED] Catalog.Валюты
|
|
||||||
&ИзменениеИКонтроль("РеквизитыРедактируемыеВГрупповойОбработке") — line 4 in ...
|
|
||||||
&Перед("ЗагрузитьКурсыВалют") — line 13 in ...
|
|
||||||
ChildObjects: 1 own attrs, 1 own TS, 3 own forms
|
|
||||||
Form.ФормаЭлемента (borrowed):
|
|
||||||
Event:OnCreateAtServer [After] -> Расш1_ПриСозданииПосле
|
|
||||||
Command:Подбор [Before] -> Расш1_ПодборПеред
|
|
||||||
Form.Расш1_МояФорма (own)
|
|
||||||
[OWN] Catalog.Расш5_Справочник1
|
|
||||||
```
|
|
||||||
|
|
||||||
Для каждой формы заимствованного объекта показывается:
|
Для каждой формы заимствованного объекта показывается:
|
||||||
- `(borrowed)` / `(own)` — заимствованная или собственная форма
|
- `(borrowed)` / `(own)` — заимствованная или собственная форма
|
||||||
- callType-события формы и элементов
|
- callType-события формы и элементов
|
||||||
@@ -44,18 +44,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File .claude/skills/cfe-init/scripts/cfe-init.ps1 -Name "МоёРасширение"
|
python ".opencode/skills/cfe-init/scripts/cfe-init.py" -Name "МоёРасширение"
|
||||||
```
|
|
||||||
|
|
||||||
## Что создаётся
|
|
||||||
|
|
||||||
```
|
|
||||||
<OutputDir>/
|
|
||||||
├── Configuration.xml # Свойства расширения
|
|
||||||
├── Languages/
|
|
||||||
│ └── Русский.xml # Язык (заимствованный)
|
|
||||||
└── Roles/ # Если не -NoRole
|
|
||||||
└── <Prefix>ОсновнаяРоль.xml
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
+24
-6
@@ -1,4 +1,4 @@
|
|||||||
# cfe-init v1.0 — Create 1C configuration extension scaffold (CFE)
|
# cfe-init v1.2 — Create 1C configuration extension scaffold (CFE)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -35,6 +35,10 @@ if (Test-Path $cfgFile) {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# MDClasses format version — inherited from the base config so the extension stays uniform
|
||||||
|
# with it (a 2.13 base must yield a 2.13 extension, else platform import rejects the mismatch).
|
||||||
|
$formatVersion = "2.17"
|
||||||
|
|
||||||
# --- Resolve ConfigPath ---
|
# --- Resolve ConfigPath ---
|
||||||
$baseLangUuid = "00000000-0000-0000-0000-000000000000"
|
$baseLangUuid = "00000000-0000-0000-0000-000000000000"
|
||||||
if ($ConfigPath) {
|
if ($ConfigPath) {
|
||||||
@@ -69,12 +73,17 @@ if ($ConfigPath) {
|
|||||||
Write-Host "[WARN] Base config language not found: $baseLangFile"
|
Write-Host "[WARN] Base config language not found: $baseLangFile"
|
||||||
}
|
}
|
||||||
|
|
||||||
# 3b. Read CompatibilityMode from base config
|
# 3b. Read CompatibilityMode and InterfaceCompatibilityMode from base config
|
||||||
$baseCfgDoc = New-Object System.Xml.XmlDocument
|
$baseCfgDoc = New-Object System.Xml.XmlDocument
|
||||||
$baseCfgDoc.PreserveWhitespace = $false
|
$baseCfgDoc.PreserveWhitespace = $false
|
||||||
$baseCfgDoc.Load((Resolve-Path $ConfigPath).Path)
|
$baseCfgDoc.Load((Resolve-Path $ConfigPath).Path)
|
||||||
$baseCfgNs = New-Object System.Xml.XmlNamespaceManager($baseCfgDoc.NameTable)
|
$baseCfgNs = New-Object System.Xml.XmlNamespaceManager($baseCfgDoc.NameTable)
|
||||||
$baseCfgNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
$baseCfgNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||||
|
$fmtVer = $baseCfgDoc.DocumentElement.GetAttribute("version")
|
||||||
|
if ($fmtVer) {
|
||||||
|
$formatVersion = $fmtVer
|
||||||
|
Write-Host "[INFO] Base config format version: $formatVersion"
|
||||||
|
}
|
||||||
$compatNode = $baseCfgDoc.SelectSingleNode("//md:Configuration/md:Properties/md:CompatibilityMode", $baseCfgNs)
|
$compatNode = $baseCfgDoc.SelectSingleNode("//md:Configuration/md:Properties/md:CompatibilityMode", $baseCfgNs)
|
||||||
if ($compatNode -and $compatNode.InnerText) {
|
if ($compatNode -and $compatNode.InnerText) {
|
||||||
$CompatibilityMode = $compatNode.InnerText.Trim()
|
$CompatibilityMode = $compatNode.InnerText.Trim()
|
||||||
@@ -82,7 +91,16 @@ if ($ConfigPath) {
|
|||||||
} else {
|
} else {
|
||||||
Write-Host "[WARN] CompatibilityMode not found in base config, using default: $CompatibilityMode"
|
Write-Host "[WARN] CompatibilityMode not found in base config, using default: $CompatibilityMode"
|
||||||
}
|
}
|
||||||
|
$ifcNode = $baseCfgDoc.SelectSingleNode("//md:Configuration/md:Properties/md:InterfaceCompatibilityMode", $baseCfgNs)
|
||||||
|
if ($ifcNode -and $ifcNode.InnerText) {
|
||||||
|
$InterfaceCompatibilityMode = $ifcNode.InnerText.Trim()
|
||||||
|
Write-Host "[INFO] Base config InterfaceCompatibilityMode: $InterfaceCompatibilityMode"
|
||||||
|
} else {
|
||||||
|
$InterfaceCompatibilityMode = "TaxiEnableVersion8_2"
|
||||||
|
Write-Host "[WARN] InterfaceCompatibilityMode not found in base config, using default: $InterfaceCompatibilityMode"
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
$InterfaceCompatibilityMode = "TaxiEnableVersion8_2"
|
||||||
Write-Host "[WARN] Language ExtendedConfigurationObject set to zeros. Use -ConfigPath to auto-resolve from base config, or fix manually before loading."
|
Write-Host "[WARN] Language ExtendedConfigurationObject set to zeros. Use -ConfigPath to auto-resolve from base config, or fix manually before loading."
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,7 +147,7 @@ $childObjectsXml += "`r`n`t`t"
|
|||||||
# --- Configuration.xml ---
|
# --- Configuration.xml ---
|
||||||
$cfgXml = @"
|
$cfgXml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject 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" version="2.17">
|
<MetaDataObject 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" version="$formatVersion">
|
||||||
<Configuration uuid="$uuidCfg">
|
<Configuration uuid="$uuidCfg">
|
||||||
<InternalInfo>
|
<InternalInfo>
|
||||||
<xr:ContainedObject>
|
<xr:ContainedObject>
|
||||||
@@ -184,7 +202,7 @@ $cfgXml = @"
|
|||||||
<Copyright/>
|
<Copyright/>
|
||||||
<VendorInformationAddress/>
|
<VendorInformationAddress/>
|
||||||
<ConfigurationInformationAddress/>
|
<ConfigurationInformationAddress/>
|
||||||
<InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode>
|
<InterfaceCompatibilityMode>$InterfaceCompatibilityMode</InterfaceCompatibilityMode>
|
||||||
</Properties>
|
</Properties>
|
||||||
<ChildObjects>$childObjectsXml</ChildObjects>
|
<ChildObjects>$childObjectsXml</ChildObjects>
|
||||||
</Configuration>
|
</Configuration>
|
||||||
@@ -194,7 +212,7 @@ $cfgXml = @"
|
|||||||
# --- Languages/Русский.xml (adopted format) ---
|
# --- Languages/Русский.xml (adopted format) ---
|
||||||
$langXml = @"
|
$langXml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject 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" version="2.17">
|
<MetaDataObject 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" version="$formatVersion">
|
||||||
<Language uuid="$uuidLang">
|
<Language uuid="$uuidLang">
|
||||||
<InternalInfo/>
|
<InternalInfo/>
|
||||||
<Properties>
|
<Properties>
|
||||||
@@ -211,7 +229,7 @@ $langXml = @"
|
|||||||
# --- Role XML ---
|
# --- Role XML ---
|
||||||
$roleXml = @"
|
$roleXml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject 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" version="2.17">
|
<MetaDataObject 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" version="$formatVersion">
|
||||||
<Role uuid="$uuidRole">
|
<Role uuid="$uuidRole">
|
||||||
<Properties>
|
<Properties>
|
||||||
<Name>$([System.Security.SecurityElement]::Escape($roleName))</Name>
|
<Name>$([System.Security.SecurityElement]::Escape($roleName))</Name>
|
||||||
+23
-6
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cfe-init v1.0 — Create 1C configuration extension scaffold (CFE)
|
# cfe-init v1.2 — Create 1C configuration extension scaffold (CFE)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
"""Generates minimal XML source files for a 1C configuration extension."""
|
"""Generates minimal XML source files for a 1C configuration extension."""
|
||||||
import sys, os, argparse, uuid
|
import sys, os, argparse, uuid
|
||||||
@@ -50,6 +50,10 @@ def main():
|
|||||||
print(f"Configuration.xml already exists: {cfg_file}", file=sys.stderr)
|
print(f"Configuration.xml already exists: {cfg_file}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
# MDClasses format version — inherited from the base config so the extension stays uniform
|
||||||
|
# with it (a 2.13 base must yield a 2.13 extension, else platform import rejects the mismatch).
|
||||||
|
format_version = "2.17"
|
||||||
|
|
||||||
# --- Resolve ConfigPath ---
|
# --- Resolve ConfigPath ---
|
||||||
base_lang_uuid = "00000000-0000-0000-0000-000000000000"
|
base_lang_uuid = "00000000-0000-0000-0000-000000000000"
|
||||||
if args.ConfigPath:
|
if args.ConfigPath:
|
||||||
@@ -84,10 +88,14 @@ def main():
|
|||||||
else:
|
else:
|
||||||
print(f"[WARN] Base config language not found: {base_lang_file}")
|
print(f"[WARN] Base config language not found: {base_lang_file}")
|
||||||
|
|
||||||
# Read CompatibilityMode from base config
|
# Read CompatibilityMode and InterfaceCompatibilityMode from base config
|
||||||
try:
|
try:
|
||||||
base_cfg_tree = ET.parse(os.path.abspath(config_path))
|
base_cfg_tree = ET.parse(os.path.abspath(config_path))
|
||||||
base_cfg_root = base_cfg_tree.getroot()
|
base_cfg_root = base_cfg_tree.getroot()
|
||||||
|
fmt_ver = base_cfg_root.get("version")
|
||||||
|
if fmt_ver:
|
||||||
|
format_version = fmt_ver
|
||||||
|
print(f"[INFO] Base config format version: {format_version}")
|
||||||
ns = {'md': 'http://v8.1c.ru/8.3/MDClasses'}
|
ns = {'md': 'http://v8.1c.ru/8.3/MDClasses'}
|
||||||
compat_node = base_cfg_root.find('.//md:Configuration/md:Properties/md:CompatibilityMode', ns)
|
compat_node = base_cfg_root.find('.//md:Configuration/md:Properties/md:CompatibilityMode', ns)
|
||||||
if compat_node is not None and compat_node.text:
|
if compat_node is not None and compat_node.text:
|
||||||
@@ -95,9 +103,18 @@ def main():
|
|||||||
print(f"[INFO] Base config CompatibilityMode: {compat}")
|
print(f"[INFO] Base config CompatibilityMode: {compat}")
|
||||||
else:
|
else:
|
||||||
print(f"[WARN] CompatibilityMode not found in base config, using default: {compat}")
|
print(f"[WARN] CompatibilityMode not found in base config, using default: {compat}")
|
||||||
|
ifc_node = base_cfg_root.find('.//md:Configuration/md:Properties/md:InterfaceCompatibilityMode', ns)
|
||||||
|
if ifc_node is not None and ifc_node.text:
|
||||||
|
ifc_mode = ifc_node.text.strip()
|
||||||
|
print(f"[INFO] Base config InterfaceCompatibilityMode: {ifc_mode}")
|
||||||
|
else:
|
||||||
|
ifc_mode = "TaxiEnableVersion8_2"
|
||||||
|
print(f"[WARN] InterfaceCompatibilityMode not found in base config, using default: {ifc_mode}")
|
||||||
except Exception:
|
except Exception:
|
||||||
print(f"[WARN] Could not parse base config, using default CompatibilityMode: {compat}")
|
print(f"[WARN] Could not parse base config, using default CompatibilityMode: {compat}")
|
||||||
|
ifc_mode = "TaxiEnableVersion8_2"
|
||||||
else:
|
else:
|
||||||
|
ifc_mode = "TaxiEnableVersion8_2"
|
||||||
print("[WARN] Language ExtendedConfigurationObject set to zeros. Use -ConfigPath to auto-resolve from base config, or fix manually before loading.")
|
print("[WARN] Language ExtendedConfigurationObject set to zeros. Use -ConfigPath to auto-resolve from base config, or fix manually before loading.")
|
||||||
|
|
||||||
# --- Generate UUIDs ---
|
# --- Generate UUIDs ---
|
||||||
@@ -146,7 +163,7 @@ def main():
|
|||||||
\t\t\t</xr:ContainedObject>\n"""
|
\t\t\t</xr:ContainedObject>\n"""
|
||||||
|
|
||||||
cfg_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
cfg_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject 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" version="2.17">
|
<MetaDataObject 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" version="{format_version}">
|
||||||
\t<Configuration uuid="{uuid_cfg}">
|
\t<Configuration uuid="{uuid_cfg}">
|
||||||
\t\t<InternalInfo>
|
\t\t<InternalInfo>
|
||||||
{contained_objects}\t\t</InternalInfo>
|
{contained_objects}\t\t</InternalInfo>
|
||||||
@@ -173,7 +190,7 @@ def main():
|
|||||||
\t\t\t<Copyright/>
|
\t\t\t<Copyright/>
|
||||||
\t\t\t<VendorInformationAddress/>
|
\t\t\t<VendorInformationAddress/>
|
||||||
\t\t\t<ConfigurationInformationAddress/>
|
\t\t\t<ConfigurationInformationAddress/>
|
||||||
\t\t\t<InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode>
|
\t\t\t<InterfaceCompatibilityMode>{ifc_mode}</InterfaceCompatibilityMode>
|
||||||
\t\t</Properties>
|
\t\t</Properties>
|
||||||
\t\t<ChildObjects>{child_objects_xml}</ChildObjects>
|
\t\t<ChildObjects>{child_objects_xml}</ChildObjects>
|
||||||
\t</Configuration>
|
\t</Configuration>
|
||||||
@@ -181,7 +198,7 @@ def main():
|
|||||||
|
|
||||||
# --- Languages/Русский.xml (adopted format) ---
|
# --- Languages/Русский.xml (adopted format) ---
|
||||||
lang_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
lang_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject 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" version="2.17">
|
<MetaDataObject 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" version="{format_version}">
|
||||||
\t<Language uuid="{uuid_lang}">
|
\t<Language uuid="{uuid_lang}">
|
||||||
\t\t<InternalInfo/>
|
\t\t<InternalInfo/>
|
||||||
\t\t<Properties>
|
\t\t<Properties>
|
||||||
@@ -196,7 +213,7 @@ def main():
|
|||||||
|
|
||||||
# --- Role XML ---
|
# --- Role XML ---
|
||||||
role_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
role_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject 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" version="2.17">
|
<MetaDataObject 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" version="{format_version}">
|
||||||
\t<Role uuid="{uuid_role}">
|
\t<Role uuid="{uuid_role}">
|
||||||
\t\t<Properties>
|
\t\t<Properties>
|
||||||
\t\t\t<Name>{esc_xml(role_name)}</Name>
|
\t\t\t<Name>{esc_xml(role_name)}</Name>
|
||||||
+1
-1
@@ -51,7 +51,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File .claude/skills/cfe-patch-method/scripts/cfe-patch-method.ps1 -ExtensionPath src -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
python ".opencode/skills/cfe-patch-method/scripts/cfe-patch-method.py" -ExtensionPath src -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
||||||
```
|
```
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
+9
-1
@@ -1,4 +1,4 @@
|
|||||||
# cfe-patch-method v1.0 — Generate method interceptor for 1C extension (CFE)
|
# cfe-patch-method v1.1 — Generate method interceptor for 1C extension (CFE)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -66,6 +66,14 @@ $typeDirMap = @{
|
|||||||
"BusinessProcess"="BusinessProcesses"; "Task"="Tasks"
|
"BusinessProcess"="BusinessProcesses"; "Task"="Tasks"
|
||||||
"InformationRegister"="InformationRegisters"; "AccumulationRegister"="AccumulationRegisters"
|
"InformationRegister"="InformationRegisters"; "AccumulationRegister"="AccumulationRegisters"
|
||||||
"AccountingRegister"="AccountingRegisters"; "CalculationRegister"="CalculationRegisters"
|
"AccountingRegister"="AccountingRegisters"; "CalculationRegister"="CalculationRegisters"
|
||||||
|
"Catalogs"="Catalogs"; "Documents"="Documents"; "Enums"="Enums"
|
||||||
|
"CommonModules"="CommonModules"; "Reports"="Reports"; "DataProcessors"="DataProcessors"
|
||||||
|
"ExchangePlans"="ExchangePlans"; "ChartsOfAccounts"="ChartsOfAccounts"
|
||||||
|
"ChartsOfCharacteristicTypes"="ChartsOfCharacteristicTypes"
|
||||||
|
"ChartsOfCalculationTypes"="ChartsOfCalculationTypes"
|
||||||
|
"BusinessProcesses"="BusinessProcesses"; "Tasks"="Tasks"
|
||||||
|
"InformationRegisters"="InformationRegisters"; "AccumulationRegisters"="AccumulationRegisters"
|
||||||
|
"AccountingRegisters"="AccountingRegisters"; "CalculationRegisters"="CalculationRegisters"
|
||||||
}
|
}
|
||||||
|
|
||||||
$parts = $ModulePath.Split(".")
|
$parts = $ModulePath.Split(".")
|
||||||
+17
-1
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cfe-patch-method v1.0 — Generate method interceptor for 1C extension (CFE)
|
# cfe-patch-method v1.1 — Generate method interceptor for 1C extension (CFE)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -84,6 +84,22 @@ def main():
|
|||||||
"AccumulationRegister": "AccumulationRegisters",
|
"AccumulationRegister": "AccumulationRegisters",
|
||||||
"AccountingRegister": "AccountingRegisters",
|
"AccountingRegister": "AccountingRegisters",
|
||||||
"CalculationRegister": "CalculationRegisters",
|
"CalculationRegister": "CalculationRegisters",
|
||||||
|
"Catalogs": "Catalogs",
|
||||||
|
"Documents": "Documents",
|
||||||
|
"Enums": "Enums",
|
||||||
|
"CommonModules": "CommonModules",
|
||||||
|
"Reports": "Reports",
|
||||||
|
"DataProcessors": "DataProcessors",
|
||||||
|
"ExchangePlans": "ExchangePlans",
|
||||||
|
"ChartsOfAccounts": "ChartsOfAccounts",
|
||||||
|
"ChartsOfCharacteristicTypes": "ChartsOfCharacteristicTypes",
|
||||||
|
"ChartsOfCalculationTypes": "ChartsOfCalculationTypes",
|
||||||
|
"BusinessProcesses": "BusinessProcesses",
|
||||||
|
"Tasks": "Tasks",
|
||||||
|
"InformationRegisters": "InformationRegisters",
|
||||||
|
"AccumulationRegisters": "AccumulationRegisters",
|
||||||
|
"AccountingRegisters": "AccountingRegisters",
|
||||||
|
"CalculationRegisters": "CalculationRegisters",
|
||||||
}
|
}
|
||||||
|
|
||||||
parts = module_path.split(".")
|
parts = module_path.split(".")
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
name: cfe-validate
|
||||||
|
description: Валидация расширения конфигурации 1С (CFE). Используй после создания или модификации расширения для проверки корректности
|
||||||
|
argument-hint: <ExtensionPath> [-Detailed] [-MaxErrors 30]
|
||||||
|
allowed-tools:
|
||||||
|
- Bash
|
||||||
|
- Read
|
||||||
|
- Glob
|
||||||
|
---
|
||||||
|
|
||||||
|
# /cfe-validate — валидация расширения конфигурации (CFE)
|
||||||
|
|
||||||
|
Проверяет структурную корректность расширения: XML-формат, свойства, состав, заимствованные объекты. Аналог `/cf-validate`, но для расширений.
|
||||||
|
|
||||||
|
## Параметры
|
||||||
|
|
||||||
|
| Параметр | Обяз. | Умолч. | Описание |
|
||||||
|
|---------------|:-----:|---------|-------------------------------------------------|
|
||||||
|
| ExtensionPath | да | — | Путь к каталогу или Configuration.xml расширения |
|
||||||
|
| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) |
|
||||||
|
| MaxErrors | нет | 30 | Остановиться после N ошибок |
|
||||||
|
| OutFile | нет | — | Записать результат в файл |
|
||||||
|
|
||||||
|
## Команда
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python ".opencode/skills/cfe-validate/scripts/cfe-validate.py" -ExtensionPath "src"
|
||||||
|
python ".opencode/skills/cfe-validate/scripts/cfe-validate.py" -ExtensionPath "src/Configuration.xml"
|
||||||
|
```
|
||||||
+6
-5
@@ -1,7 +1,8 @@
|
|||||||
# cfe-validate v1.3 — Validate 1C configuration extension structure (CFE)
|
# cfe-validate v1.4 — Validate 1C configuration extension structure (CFE)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
|
[Alias('Path')]
|
||||||
[string]$ExtensionPath,
|
[string]$ExtensionPath,
|
||||||
|
|
||||||
[switch]$Detailed,
|
[switch]$Detailed,
|
||||||
@@ -145,10 +146,10 @@ $childTypeDirMap = @{
|
|||||||
|
|
||||||
# Valid enum values for extension properties
|
# Valid enum values for extension properties
|
||||||
$validEnumValues = @{
|
$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")
|
"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","Version8_5_1")
|
||||||
"DefaultRunMode" = @("ManagedApplication","OrdinaryApplication","Auto")
|
"DefaultRunMode" = @("ManagedApplication","OrdinaryApplication","Auto")
|
||||||
"ScriptVariant" = @("Russian","English")
|
"ScriptVariant" = @("Russian","English")
|
||||||
"InterfaceCompatibilityMode" = @("Taxi","TaxiEnableVersion8_2","Version8_2")
|
"InterfaceCompatibilityMode" = @("Version8_2","Version8_2EnableTaxi","Taxi","TaxiEnableVersion8_2","TaxiEnableVersion8_5","Version8_5EnableTaxi","Version8_5")
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- 1. Parse XML ---
|
# --- 1. Parse XML ---
|
||||||
@@ -196,8 +197,8 @@ if ($root.NamespaceURI -ne $expectedNs) {
|
|||||||
$version = $root.GetAttribute("version")
|
$version = $root.GetAttribute("version")
|
||||||
if (-not $version) {
|
if (-not $version) {
|
||||||
Report-Warn "1. Missing version attribute on MetaDataObject"
|
Report-Warn "1. Missing version attribute on MetaDataObject"
|
||||||
} elseif ($version -ne "2.17" -and $version -ne "2.20") {
|
} elseif ($version -ne "2.17" -and $version -ne "2.20" -and $version -ne "2.21") {
|
||||||
Report-Warn "1. Unusual version '$version' (expected 2.17 or 2.20)"
|
Report-Warn "1. Unusual version '$version' (expected 2.17, 2.20 or 2.21)"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Must have Configuration child
|
# Must have Configuration child
|
||||||
+9
-6
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cfe-validate v1.3 — Validate 1C configuration extension XML structure (CFE)
|
# cfe-validate v1.4 — Validate 1C configuration extension XML structure (CFE)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
"""Validates extension Configuration.xml: root, InternalInfo, extension properties, ChildObjects, borrowed objects."""
|
"""Validates extension Configuration.xml: root, InternalInfo, extension properties, ChildObjects, borrowed objects."""
|
||||||
import sys, os, argparse, re
|
import sys, os, argparse, re
|
||||||
@@ -82,11 +82,14 @@ VALID_ENUM_VALUES = {
|
|||||||
'Version8_3_11', 'Version8_3_12', 'Version8_3_13', 'Version8_3_14', 'Version8_3_15',
|
'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_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_21', 'Version8_3_22', 'Version8_3_23', 'Version8_3_24', 'Version8_3_25',
|
||||||
'Version8_3_26', 'Version8_3_27', 'Version8_3_28',
|
'Version8_3_26', 'Version8_3_27', 'Version8_3_28', 'Version8_5_1',
|
||||||
],
|
],
|
||||||
'DefaultRunMode': ['ManagedApplication', 'OrdinaryApplication', 'Auto'],
|
'DefaultRunMode': ['ManagedApplication', 'OrdinaryApplication', 'Auto'],
|
||||||
'ScriptVariant': ['Russian', 'English'],
|
'ScriptVariant': ['Russian', 'English'],
|
||||||
'InterfaceCompatibilityMode': ['Taxi', 'TaxiEnableVersion8_2', 'Version8_2'],
|
'InterfaceCompatibilityMode': [
|
||||||
|
'Version8_2', 'Version8_2EnableTaxi', 'Taxi', 'TaxiEnableVersion8_2',
|
||||||
|
'TaxiEnableVersion8_5', 'Version8_5EnableTaxi', 'Version8_5',
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
EXPECTED_NS = 'http://v8.1c.ru/8.3/MDClasses'
|
EXPECTED_NS = 'http://v8.1c.ru/8.3/MDClasses'
|
||||||
@@ -147,7 +150,7 @@ def main():
|
|||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description='Validate 1C configuration extension XML structure (CFE)', allow_abbrev=False
|
description='Validate 1C configuration extension XML structure (CFE)', allow_abbrev=False
|
||||||
)
|
)
|
||||||
parser.add_argument('-ExtensionPath', dest='ExtensionPath', required=True)
|
parser.add_argument('-ExtensionPath', '-Path', dest='ExtensionPath', required=True)
|
||||||
parser.add_argument('-Detailed', action='store_true')
|
parser.add_argument('-Detailed', action='store_true')
|
||||||
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
|
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
|
||||||
parser.add_argument('-OutFile', dest='OutFile', default='')
|
parser.add_argument('-OutFile', dest='OutFile', default='')
|
||||||
@@ -213,8 +216,8 @@ def main():
|
|||||||
version = root.get('version', '')
|
version = root.get('version', '')
|
||||||
if not version:
|
if not version:
|
||||||
r.warn('1. Missing version attribute on MetaDataObject')
|
r.warn('1. Missing version attribute on MetaDataObject')
|
||||||
elif version not in ('2.17', '2.20'):
|
elif version not in ('2.17', '2.20', '2.21'):
|
||||||
r.warn(f"1. Unusual version '{version}' (expected 2.17 or 2.20)")
|
r.warn(f"1. Unusual version '{version}' (expected 2.17, 2.20 or 2.21)")
|
||||||
|
|
||||||
# Must have Configuration child
|
# Must have Configuration child
|
||||||
cfg_node = None
|
cfg_node = None
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: db-create
|
name: db-create
|
||||||
description: Создание информационной базы 1С. Используй когда пользователь просит создать базу, новую ИБ, пустую базу
|
description: Создание информационной базы 1С. Используй когда нужно создать базу, новую ИБ, пустую базу
|
||||||
argument-hint: <path|name>
|
argument-hint: <path|name>
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash
|
- Bash
|
||||||
@@ -25,20 +25,20 @@ allowed-tools:
|
|||||||
## Параметры подключения
|
## Параметры подключения
|
||||||
|
|
||||||
Прочитай `.v8-project.json` из корня проекта для `v8path` (путь к платформе).
|
Прочитай `.v8-project.json` из корня проекта для `v8path` (путь к платформе).
|
||||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||||
После создания базы предложи зарегистрировать через `/db-list add`.
|
После создания базы предложи зарегистрировать через `/db-list add`.
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-create/scripts/db-create.ps1 <параметры>
|
python ".opencode/skills/db-create/scripts/db-create.py" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
| Параметр | Обязательный | Описание |
|
||||||
|----------|:------------:|----------|
|
|----------|:------------:|----------|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||||
| `-InfoBasePath <путь>` | * | Путь к файловой базе |
|
| `-InfoBasePath <путь>` | * | Путь к файловой базе |
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
@@ -48,31 +48,23 @@ powershell.exe -NoProfile -File .claude/skills/db-create/scripts/db-create.ps1 <
|
|||||||
|
|
||||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||||
|
|
||||||
## Коды возврата
|
|
||||||
|
|
||||||
| Код | Описание |
|
|
||||||
|-----|----------|
|
|
||||||
| 0 | Успешно |
|
|
||||||
| 1 | Ошибка (см. лог) |
|
|
||||||
|
|
||||||
## После создания
|
## После создания
|
||||||
|
|
||||||
1. Прочитай лог-файл и покажи результат
|
Предложи зарегистрировать базу в `.v8-project.json` (через `/db-list add`)
|
||||||
2. Предложи зарегистрировать базу в `.v8-project.json` (через `/db-list add`)
|
|
||||||
3. Если указан шаблон `/UseTemplate` — предупреди что конфигурация будет загружена из шаблона
|
3. Если указан шаблон `/UseTemplate` — предупреди что конфигурация будет загружена из шаблона
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Создать файловую базу
|
# Создать файловую базу
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-create/scripts/db-create.ps1 -InfoBasePath "C:\Bases\NewDB"
|
python ".opencode/skills/db-create/scripts/db-create.py" -InfoBasePath "C:\Bases\NewDB"
|
||||||
|
|
||||||
# Создать серверную базу
|
# Создать серверную базу
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-create/scripts/db-create.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test"
|
python ".opencode/skills/db-create/scripts/db-create.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test"
|
||||||
|
|
||||||
# Создать из шаблона CF
|
# Создать из шаблона CF
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-create/scripts/db-create.ps1 -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf"
|
python ".opencode/skills/db-create/scripts/db-create.py" -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf"
|
||||||
|
|
||||||
# Создать и добавить в список баз
|
# Создать и добавить в список баз
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-create/scripts/db-create.ps1 -InfoBasePath "C:\Bases\NewDB" -AddToList -ListName "Новая база"
|
python ".opencode/skills/db-create/scripts/db-create.py" -InfoBasePath "C:\Bases\NewDB" -AddToList -ListName "Новая база"
|
||||||
```
|
```
|
||||||
+92
-6
@@ -1,5 +1,6 @@
|
|||||||
# db-create v1.0 — Create 1C information base
|
# db-create v1.6 — Create 1C information base
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Создание информационной базы 1С
|
Создание информационной базы 1С
|
||||||
@@ -67,25 +68,85 @@ $OutputEncoding = [System.Text.Encoding]::UTF8
|
|||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
|
function Find-ProjectV8Path {
|
||||||
|
$dir = (Get-Location).Path
|
||||||
|
while ($dir) {
|
||||||
|
$pf = Join-Path $dir ".v8-project.json"
|
||||||
|
if (Test-Path $pf) {
|
||||||
|
try {
|
||||||
|
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
if ($j.v8path) { return [string]$j.v8path }
|
||||||
|
} catch {}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
$parent = Split-Path $dir -Parent
|
||||||
|
if (-not $parent -or $parent -eq $dir) { break }
|
||||||
|
$dir = $parent
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
if (-not $V8Path) {
|
if (-not $V8Path) {
|
||||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
$V8Path = Find-ProjectV8Path
|
||||||
|
}
|
||||||
|
if (-not $V8Path) {
|
||||||
|
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
||||||
|
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
||||||
|
Select-Object -First 1
|
||||||
if ($found) {
|
if ($found) {
|
||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
} elseif (Test-Path $V8Path -PathType Container) {
|
}
|
||||||
|
if (Test-Path $V8Path -PathType Container) {
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
|
function Invoke-IbcmdProcess {
|
||||||
|
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
||||||
|
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
||||||
|
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
||||||
|
param([string]$Exe, [string[]]$IbArgs)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
try {
|
||||||
|
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
} catch {}
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
$out = $p.StandardOutput.ReadToEnd()
|
||||||
|
$err = $p.StandardError.ReadToEnd()
|
||||||
|
$p.WaitForExit()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
if ($engine -eq "ibcmd") {
|
||||||
|
if (-not $InfoBasePath) {
|
||||||
|
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -101,6 +162,31 @@ $tempDir = Join-Path $env:TEMP "db_create_$(Get-Random)"
|
|||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
# --- ibcmd branch (file infobase only) ---
|
||||||
|
$arguments = @("infobase", "create", "--db-path=$InfoBasePath", "--create-database")
|
||||||
|
if ($UseTemplate) {
|
||||||
|
if ([System.IO.Path]::GetExtension($UseTemplate) -ieq ".dt") {
|
||||||
|
$arguments += "--restore=$UseTemplate"
|
||||||
|
} else {
|
||||||
|
$arguments += "--load=$UseTemplate", "--apply"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$arguments += "--data=$tempDir"
|
||||||
|
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
if ($exitCode -eq 0) {
|
||||||
|
Write-Host "Information base created successfully: $InfoBasePath" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "Error creating information base (code: $exitCode)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
if ($output) { Write-Host ($output | Out-String) }
|
||||||
|
exit $exitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1cv8 branch ---
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
$arguments = @("CREATEINFOBASE")
|
$arguments = @("CREATEINFOBASE")
|
||||||
|
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# db-create v1.6 — Create 1C information base
|
||||||
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import atexit
|
||||||
|
import glob
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
|
def _find_project_v8path():
|
||||||
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
|
d = os.getcwd()
|
||||||
|
while True:
|
||||||
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pf):
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
v = data.get("v8path")
|
||||||
|
if v:
|
||||||
|
return v
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return None
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
|
def _version_key(p):
|
||||||
|
"""Numeric sort key from version dir name."""
|
||||||
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_v8path(v8path):
|
||||||
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
|
if not v8path:
|
||||||
|
v8path = _find_project_v8path()
|
||||||
|
if not v8path:
|
||||||
|
if os.name == "nt":
|
||||||
|
candidates = (
|
||||||
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
|
if candidates:
|
||||||
|
v8path = max(candidates, key=_version_key)
|
||||||
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
|
else:
|
||||||
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if os.path.isdir(v8path):
|
||||||
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
|
if not os.path.isfile(v8path):
|
||||||
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Create 1C information base",
|
||||||
|
allow_abbrev=False,
|
||||||
|
)
|
||||||
|
parser.add_argument("-V8Path", default="")
|
||||||
|
parser.add_argument("-InfoBasePath", default="")
|
||||||
|
parser.add_argument("-InfoBaseServer", default="")
|
||||||
|
parser.add_argument("-InfoBaseRef", default="")
|
||||||
|
parser.add_argument("-UseTemplate", default="")
|
||||||
|
parser.add_argument("-AddToList", action="store_true")
|
||||||
|
parser.add_argument("-ListName", default="")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
|
# --- Validate connection ---
|
||||||
|
if engine == "ibcmd":
|
||||||
|
if not args.InfoBasePath:
|
||||||
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# --- Validate template ---
|
||||||
|
if args.UseTemplate and not os.path.exists(args.UseTemplate):
|
||||||
|
print(f"Error: template file not found: {args.UseTemplate}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# --- ibcmd branch (file infobase only) ---
|
||||||
|
if engine == "ibcmd":
|
||||||
|
arguments = ["infobase", "create", f"--db-path={args.InfoBasePath}", "--create-database"]
|
||||||
|
if args.UseTemplate:
|
||||||
|
if os.path.splitext(args.UseTemplate)[1].lower() == ".dt":
|
||||||
|
arguments.append(f"--restore={args.UseTemplate}")
|
||||||
|
else:
|
||||||
|
arguments.extend([f"--load={args.UseTemplate}", "--apply"])
|
||||||
|
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
||||||
|
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
||||||
|
arguments.append(f"--data={ib_data}")
|
||||||
|
print(f"Running: ibcmd {' '.join(arguments)}")
|
||||||
|
result = run_ibcmd([v8path] + arguments, warn_no_user=False)
|
||||||
|
if result.returncode == 0:
|
||||||
|
print(f"Information base created successfully: {args.InfoBasePath}")
|
||||||
|
else:
|
||||||
|
print(f"Error creating information base (code: {result.returncode})", file=sys.stderr)
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
sys.exit(result.returncode)
|
||||||
|
|
||||||
|
# --- Temp dir ---
|
||||||
|
temp_dir = os.path.join(tempfile.gettempdir(), f"db_create_{random.randint(0, 999999)}")
|
||||||
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# --- Build arguments ---
|
||||||
|
arguments = ["CREATEINFOBASE"]
|
||||||
|
|
||||||
|
if args.InfoBaseServer and args.InfoBaseRef:
|
||||||
|
# No embedded quotes: subprocess quotes the whole token; 1C's argv parser
|
||||||
|
# strips outer quotes. Inner quotes get escaped by list2cmdline and break parsing.
|
||||||
|
arguments.append(f'Srvr={args.InfoBaseServer};Ref={args.InfoBaseRef}')
|
||||||
|
else:
|
||||||
|
arguments.append(f'File={args.InfoBasePath}')
|
||||||
|
|
||||||
|
# --- Template ---
|
||||||
|
if args.UseTemplate:
|
||||||
|
arguments.extend(["/UseTemplate", args.UseTemplate])
|
||||||
|
|
||||||
|
# --- Add to list ---
|
||||||
|
if args.AddToList:
|
||||||
|
if args.ListName:
|
||||||
|
arguments.extend(["/AddToList", args.ListName])
|
||||||
|
else:
|
||||||
|
arguments.append("/AddToList")
|
||||||
|
|
||||||
|
# --- Output ---
|
||||||
|
out_file = os.path.join(temp_dir, "create_log.txt")
|
||||||
|
arguments.extend(["/Out", out_file])
|
||||||
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
|
||||||
|
# --- Execute ---
|
||||||
|
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
||||||
|
result = subprocess.run(
|
||||||
|
[v8path] + arguments,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
exit_code = result.returncode
|
||||||
|
|
||||||
|
# --- Result ---
|
||||||
|
if exit_code == 0:
|
||||||
|
if args.InfoBaseServer and args.InfoBaseRef:
|
||||||
|
print(f"Information base created successfully: {args.InfoBaseServer}/{args.InfoBaseRef}")
|
||||||
|
else:
|
||||||
|
print(f"Information base created successfully: {args.InfoBasePath}")
|
||||||
|
else:
|
||||||
|
print(f"Error creating information base (code: {exit_code})", file=sys.stderr)
|
||||||
|
|
||||||
|
if os.path.isfile(out_file):
|
||||||
|
try:
|
||||||
|
with open(out_file, "r", encoding="utf-8-sig") as f:
|
||||||
|
log_content = f.read()
|
||||||
|
if log_content:
|
||||||
|
print("--- Log ---")
|
||||||
|
print(log_content)
|
||||||
|
print("--- End ---")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
sys.exit(exit_code)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if os.path.isdir(temp_dir):
|
||||||
|
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: db-dump-cf
|
name: db-dump-cf
|
||||||
description: Выгрузка конфигурации 1С в CF-файл. Используй когда пользователь просит выгрузить конфигурацию в CF, сохранить конфигурацию, сделать бэкап CF
|
description: Выгрузка конфигурации 1С в CF-файл. Используй когда нужно выгрузить конфигурацию в CF, сохранить конфигурацию, сделать бэкап CF
|
||||||
argument-hint: "[database] [output.cf]"
|
argument-hint: "[database] [output.cf]"
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash
|
- Bash
|
||||||
@@ -28,21 +28,21 @@ allowed-tools:
|
|||||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||||
4. Если ветка не совпала — используй `default`
|
4. Если ветка не совпала — используй `default`
|
||||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||||
Если файла нет — предложи `/db-list add`.
|
Если файла нет — предложи `/db-list add`.
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-dump-cf/scripts/db-dump-cf.ps1 <параметры>
|
python ".opencode/skills/db-dump-cf/scripts/db-dump-cf.py" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
| Параметр | Обязательный | Описание |
|
||||||
|----------|:------------:|----------|
|
|----------|:------------:|----------|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
@@ -54,26 +54,15 @@ powershell.exe -NoProfile -File .claude/skills/db-dump-cf/scripts/db-dump-cf.ps1
|
|||||||
|
|
||||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||||
|
|
||||||
## Коды возврата
|
|
||||||
|
|
||||||
| Код | Описание |
|
|
||||||
|-----|----------|
|
|
||||||
| 0 | Успешно |
|
|
||||||
| 1 | Ошибка (см. лог) |
|
|
||||||
|
|
||||||
## После выполнения
|
|
||||||
|
|
||||||
Прочитай лог-файл и покажи результат. Если есть ошибки — покажи содержимое лога.
|
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Выгрузка конфигурации (файловая база)
|
# Выгрузка конфигурации (файловая база)
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-dump-cf/scripts/db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\config.cf"
|
python ".opencode/skills/db-dump-cf/scripts/db-dump-cf.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\config.cf"
|
||||||
|
|
||||||
# Серверная база
|
# Серверная база
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-dump-cf/scripts/db-dump-cf.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "config.cf"
|
python ".opencode/skills/db-dump-cf/scripts/db-dump-cf.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "config.cf"
|
||||||
|
|
||||||
# Выгрузка расширения
|
# Выгрузка расширения
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-dump-cf/scripts/db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "ext.cfe" -Extension "МоёРасширение"
|
python ".opencode/skills/db-dump-cf/scripts/db-dump-cf.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "ext.cfe" -Extension "МоёРасширение"
|
||||||
```
|
```
|
||||||
+93
-6
@@ -1,5 +1,6 @@
|
|||||||
# db-dump-cf v1.0 — Dump 1C configuration to CF file
|
# db-dump-cf v1.6 — Dump 1C configuration to CF file
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Выгрузка конфигурации 1С в CF-файл
|
Выгрузка конфигурации 1С в CF-файл
|
||||||
@@ -76,25 +77,85 @@ $OutputEncoding = [System.Text.Encoding]::UTF8
|
|||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
|
function Find-ProjectV8Path {
|
||||||
|
$dir = (Get-Location).Path
|
||||||
|
while ($dir) {
|
||||||
|
$pf = Join-Path $dir ".v8-project.json"
|
||||||
|
if (Test-Path $pf) {
|
||||||
|
try {
|
||||||
|
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
if ($j.v8path) { return [string]$j.v8path }
|
||||||
|
} catch {}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
$parent = Split-Path $dir -Parent
|
||||||
|
if (-not $parent -or $parent -eq $dir) { break }
|
||||||
|
$dir = $parent
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
if (-not $V8Path) {
|
if (-not $V8Path) {
|
||||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
$V8Path = Find-ProjectV8Path
|
||||||
|
}
|
||||||
|
if (-not $V8Path) {
|
||||||
|
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
||||||
|
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
||||||
|
Select-Object -First 1
|
||||||
if ($found) {
|
if ($found) {
|
||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
} elseif (Test-Path $V8Path -PathType Container) {
|
}
|
||||||
|
if (Test-Path $V8Path -PathType Container) {
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
|
function Invoke-IbcmdProcess {
|
||||||
|
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
||||||
|
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
||||||
|
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
||||||
|
param([string]$Exe, [string[]]$IbArgs)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
try {
|
||||||
|
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
} catch {}
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
$out = $p.StandardOutput.ReadToEnd()
|
||||||
|
$err = $p.StandardError.ReadToEnd()
|
||||||
|
$p.WaitForExit()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
if ($engine -eq "ibcmd") {
|
||||||
|
if (-not $InfoBasePath) {
|
||||||
|
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -110,6 +171,32 @@ $tempDir = Join-Path $env:TEMP "db_dump_cf_$(Get-Random)"
|
|||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
# --- ibcmd branch (file infobase only) ---
|
||||||
|
if ($AllExtensions) {
|
||||||
|
Write-Host "Error: ibcmd config save does not support -AllExtensions (use -Extension)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
$arguments = @("infobase", "config", "save", "--db-path=$InfoBasePath")
|
||||||
|
if ($Extension) { $arguments += "--extension=$Extension" }
|
||||||
|
$arguments += "$OutputFile"
|
||||||
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
|
$arguments += "--data=$tempDir"
|
||||||
|
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
if ($exitCode -eq 0) {
|
||||||
|
Write-Host "Configuration dumped successfully to: $OutputFile" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
if ($output) { Write-Host ($output | Out-String) }
|
||||||
|
exit $exitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1cv8 branch ---
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
$arguments = @("DESIGNER")
|
$arguments = @("DESIGNER")
|
||||||
|
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# db-dump-cf v1.6 — Dump 1C configuration to CF file
|
||||||
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import atexit
|
||||||
|
import glob
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
|
def _find_project_v8path():
|
||||||
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
|
d = os.getcwd()
|
||||||
|
while True:
|
||||||
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pf):
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
v = data.get("v8path")
|
||||||
|
if v:
|
||||||
|
return v
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return None
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
|
def _version_key(p):
|
||||||
|
"""Numeric sort key from version dir name."""
|
||||||
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_v8path(v8path):
|
||||||
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
|
if not v8path:
|
||||||
|
v8path = _find_project_v8path()
|
||||||
|
if not v8path:
|
||||||
|
if os.name == "nt":
|
||||||
|
candidates = (
|
||||||
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
|
if candidates:
|
||||||
|
v8path = max(candidates, key=_version_key)
|
||||||
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
|
else:
|
||||||
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if os.path.isdir(v8path):
|
||||||
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
|
if not os.path.isfile(v8path):
|
||||||
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Dump 1C configuration to CF file",
|
||||||
|
allow_abbrev=False,
|
||||||
|
)
|
||||||
|
parser.add_argument("-V8Path", default="")
|
||||||
|
parser.add_argument("-InfoBasePath", default="")
|
||||||
|
parser.add_argument("-InfoBaseServer", default="")
|
||||||
|
parser.add_argument("-InfoBaseRef", default="")
|
||||||
|
parser.add_argument("-UserName", default="")
|
||||||
|
parser.add_argument("-Password", default="")
|
||||||
|
parser.add_argument("-OutputFile", required=True)
|
||||||
|
parser.add_argument("-Extension", default="")
|
||||||
|
parser.add_argument("-AllExtensions", action="store_true")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
|
# --- Validate connection ---
|
||||||
|
if engine == "ibcmd":
|
||||||
|
if not args.InfoBasePath:
|
||||||
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# --- Ensure output directory exists ---
|
||||||
|
out_dir = os.path.dirname(args.OutputFile)
|
||||||
|
if out_dir and not os.path.isdir(out_dir):
|
||||||
|
os.makedirs(out_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# --- ibcmd branch (file infobase only) ---
|
||||||
|
if engine == "ibcmd":
|
||||||
|
if args.AllExtensions:
|
||||||
|
print("Error: ibcmd config save does not support -AllExtensions (use -Extension)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
arguments = ["infobase", "config", "save", f"--db-path={args.InfoBasePath}"]
|
||||||
|
if args.Extension:
|
||||||
|
arguments.append(f"--extension={args.Extension}")
|
||||||
|
arguments.append(args.OutputFile)
|
||||||
|
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
||||||
|
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
||||||
|
if args.UserName:
|
||||||
|
arguments.append(f"--user={args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
arguments.append(f"--password={args.Password}")
|
||||||
|
arguments.append(f"--data={ib_data}")
|
||||||
|
print(f"Running: ibcmd {' '.join(arguments)}")
|
||||||
|
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||||
|
if result.returncode == 0:
|
||||||
|
print(f"Configuration dumped successfully to: {args.OutputFile}")
|
||||||
|
else:
|
||||||
|
print(f"Error dumping configuration (code: {result.returncode})", file=sys.stderr)
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
sys.exit(result.returncode)
|
||||||
|
|
||||||
|
# --- Temp dir ---
|
||||||
|
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_cf_{random.randint(0, 999999)}")
|
||||||
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# --- Build arguments ---
|
||||||
|
arguments = ["DESIGNER"]
|
||||||
|
|
||||||
|
if args.InfoBaseServer and args.InfoBaseRef:
|
||||||
|
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
|
||||||
|
else:
|
||||||
|
arguments.extend(["/F", args.InfoBasePath])
|
||||||
|
|
||||||
|
if args.UserName:
|
||||||
|
arguments.append(f"/N{args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
arguments.append(f"/P{args.Password}")
|
||||||
|
|
||||||
|
arguments.extend(["/DumpCfg", args.OutputFile])
|
||||||
|
|
||||||
|
# --- Extensions ---
|
||||||
|
if args.Extension:
|
||||||
|
arguments.extend(["-Extension", args.Extension])
|
||||||
|
elif args.AllExtensions:
|
||||||
|
arguments.append("-AllExtensions")
|
||||||
|
|
||||||
|
# --- Output ---
|
||||||
|
out_file = os.path.join(temp_dir, "dump_cf_log.txt")
|
||||||
|
arguments.extend(["/Out", out_file])
|
||||||
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
|
||||||
|
# --- Execute ---
|
||||||
|
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
||||||
|
result = subprocess.run(
|
||||||
|
[v8path] + arguments,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
exit_code = result.returncode
|
||||||
|
|
||||||
|
# --- Result ---
|
||||||
|
if exit_code == 0:
|
||||||
|
print(f"Configuration dumped successfully to: {args.OutputFile}")
|
||||||
|
else:
|
||||||
|
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
|
||||||
|
|
||||||
|
if os.path.isfile(out_file):
|
||||||
|
try:
|
||||||
|
with open(out_file, "r", encoding="utf-8-sig") as f:
|
||||||
|
log_content = f.read()
|
||||||
|
if log_content:
|
||||||
|
print("--- Log ---")
|
||||||
|
print(log_content)
|
||||||
|
print("--- End ---")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
sys.exit(exit_code)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if os.path.isdir(temp_dir):
|
||||||
|
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
---
|
||||||
|
name: db-dump-dt
|
||||||
|
description: Выгрузка информационной базы 1С в DT-файл (вся база — конфигурация + данные). Используй когда нужно выгрузить информационную базу, выгрузить архив базы, сделать бэкап, выгрузить dt
|
||||||
|
argument-hint: "[database] [output.dt]"
|
||||||
|
allowed-tools:
|
||||||
|
- Bash
|
||||||
|
- Read
|
||||||
|
- Glob
|
||||||
|
- AskUserQuestion
|
||||||
|
---
|
||||||
|
|
||||||
|
# /db-dump-dt — Выгрузка информационной базы в DT-файл
|
||||||
|
|
||||||
|
Выгружает информационную базу целиком (конфигурация **+ данные**) в DT-файл — полный снимок ИБ.
|
||||||
|
|
||||||
|
> В отличие от `/db-dump-cf` (только конфигурация), `.dt` содержит **всю базу**: данные,
|
||||||
|
> настройки, пользователей. Это бэкап/точка отката, а не выгрузка метаданных.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
/db-dump-dt [database] [output.dt]
|
||||||
|
/db-dump-dt dev backup.dt
|
||||||
|
/db-dump-dt — база по умолчанию, имя файла по базе и дате
|
||||||
|
```
|
||||||
|
|
||||||
|
## Параметры подключения
|
||||||
|
|
||||||
|
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу:
|
||||||
|
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
|
||||||
|
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||||
|
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||||
|
4. Если ветка не совпала — используй `default`
|
||||||
|
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||||
|
Если файла нет — предложи `/db-list add`.
|
||||||
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
|
|
||||||
|
## Команда
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python ".opencode/skills/db-dump-dt/scripts/db-dump-dt.py" <параметры>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Параметры скрипта
|
||||||
|
|
||||||
|
| Параметр | Обязательный | Описание |
|
||||||
|
|----------|:------------:|----------|
|
||||||
|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||||
|
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||||
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
|
| `-UserName <имя>` | нет | Имя пользователя |
|
||||||
|
| `-Password <пароль>` | нет | Пароль |
|
||||||
|
| `-OutputFile <путь>` | да | Путь к выходному DT-файлу |
|
||||||
|
|
||||||
|
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||||
|
|
||||||
|
## Примеры
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Выгрузка ИБ (файловая база)
|
||||||
|
python ".opencode/skills/db-dump-dt/scripts/db-dump-dt.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\base.dt"
|
||||||
|
|
||||||
|
# Серверная база
|
||||||
|
python ".opencode/skills/db-dump-dt/scripts/db-dump-dt.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "base.dt"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Связанные навыки
|
||||||
|
|
||||||
|
- `/db-load-dt` — загрузка ИБ из DT (обратная операция)
|
||||||
|
- `/db-dump-cf` — выгрузка только конфигурации (без данных)
|
||||||
|
- `/db-create` — создать новую базу (в т.ч. из DT-шаблона)
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
# db-dump-dt v1.5 — Dump 1C information base to DT file
|
||||||
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Выгрузка информационной базы 1С в DT-файл
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Выгружает информационную базу целиком (конфигурация + данные) в DT-файл.
|
||||||
|
|
||||||
|
.PARAMETER V8Path
|
||||||
|
Путь к каталогу bin платформы или к 1cv8.exe
|
||||||
|
|
||||||
|
.PARAMETER InfoBasePath
|
||||||
|
Путь к файловой информационной базе
|
||||||
|
|
||||||
|
.PARAMETER InfoBaseServer
|
||||||
|
Сервер 1С (для серверной базы)
|
||||||
|
|
||||||
|
.PARAMETER InfoBaseRef
|
||||||
|
Имя базы на сервере
|
||||||
|
|
||||||
|
.PARAMETER UserName
|
||||||
|
Имя пользователя 1С
|
||||||
|
|
||||||
|
.PARAMETER Password
|
||||||
|
Пароль пользователя
|
||||||
|
|
||||||
|
.PARAMETER OutputFile
|
||||||
|
Путь к выходному DT-файлу
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\db-dump-dt.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "backup.dt"
|
||||||
|
#>
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string]$V8Path,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string]$InfoBasePath,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string]$InfoBaseServer,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string]$InfoBaseRef,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string]$UserName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string]$Password,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$true)]
|
||||||
|
[string]$OutputFile
|
||||||
|
)
|
||||||
|
|
||||||
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
# --- Resolve V8Path ---
|
||||||
|
function Find-ProjectV8Path {
|
||||||
|
$dir = (Get-Location).Path
|
||||||
|
while ($dir) {
|
||||||
|
$pf = Join-Path $dir ".v8-project.json"
|
||||||
|
if (Test-Path $pf) {
|
||||||
|
try {
|
||||||
|
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
if ($j.v8path) { return [string]$j.v8path }
|
||||||
|
} catch {}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
$parent = Split-Path $dir -Parent
|
||||||
|
if (-not $parent -or $parent -eq $dir) { break }
|
||||||
|
$dir = $parent
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $V8Path) {
|
||||||
|
$V8Path = Find-ProjectV8Path
|
||||||
|
}
|
||||||
|
if (-not $V8Path) {
|
||||||
|
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
||||||
|
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
||||||
|
Select-Object -First 1
|
||||||
|
if ($found) {
|
||||||
|
$V8Path = $found.FullName
|
||||||
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
|
} else {
|
||||||
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Test-Path $V8Path -PathType Container) {
|
||||||
|
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path $V8Path)) {
|
||||||
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
|
function Invoke-IbcmdProcess {
|
||||||
|
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
||||||
|
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
||||||
|
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
||||||
|
param([string]$Exe, [string[]]$IbArgs)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
try {
|
||||||
|
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
} catch {}
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
$out = $p.StandardOutput.ReadToEnd()
|
||||||
|
$err = $p.StandardError.ReadToEnd()
|
||||||
|
$p.WaitForExit()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
|
# --- Validate connection ---
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
if (-not $InfoBasePath) {
|
||||||
|
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||||
|
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Ensure output directory exists ---
|
||||||
|
$outDir = Split-Path $OutputFile -Parent
|
||||||
|
if ($outDir -and -not (Test-Path $outDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $outDir -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Temp dir ---
|
||||||
|
$tempDir = Join-Path $env:TEMP "db_dump_dt_$(Get-Random)"
|
||||||
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
# --- ibcmd branch (file infobase only) ---
|
||||||
|
$arguments = @("infobase", "dump", "--db-path=$InfoBasePath")
|
||||||
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
|
$arguments += "$OutputFile"
|
||||||
|
|
||||||
|
$arguments += "--data=$tempDir"
|
||||||
|
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
if ($exitCode -eq 0) {
|
||||||
|
Write-Host "Information base dumped successfully to: $OutputFile" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "Error dumping information base (code: $exitCode)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
if ($output) { Write-Host ($output | Out-String) }
|
||||||
|
exit $exitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1cv8 branch ---
|
||||||
|
# --- Build arguments ---
|
||||||
|
$arguments = @("DESIGNER")
|
||||||
|
|
||||||
|
if ($InfoBaseServer -and $InfoBaseRef) {
|
||||||
|
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
|
||||||
|
} else {
|
||||||
|
$arguments += "/F", "`"$InfoBasePath`""
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($UserName) { $arguments += "/N`"$UserName`"" }
|
||||||
|
if ($Password) { $arguments += "/P`"$Password`"" }
|
||||||
|
|
||||||
|
$arguments += "/DumpIB", "`"$OutputFile`""
|
||||||
|
|
||||||
|
# --- Output ---
|
||||||
|
$outFile = Join-Path $tempDir "dump_dt_log.txt"
|
||||||
|
$arguments += "/Out", "`"$outFile`""
|
||||||
|
$arguments += "/DisableStartupDialogs"
|
||||||
|
|
||||||
|
# --- Execute ---
|
||||||
|
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
||||||
|
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
||||||
|
$exitCode = $process.ExitCode
|
||||||
|
|
||||||
|
# --- Result ---
|
||||||
|
if ($exitCode -eq 0) {
|
||||||
|
Write-Host "Information base dumped successfully to: $OutputFile" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "Error dumping information base (code: $exitCode)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Path $outFile) {
|
||||||
|
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
|
||||||
|
if ($logContent) {
|
||||||
|
Write-Host "--- Log ---"
|
||||||
|
Write-Host $logContent
|
||||||
|
Write-Host "--- End ---"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exit $exitCode
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
if (Test-Path $tempDir) {
|
||||||
|
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# db-dump-dt v1.5 — Dump 1C information base to DT file
|
||||||
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import atexit
|
||||||
|
import glob
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
|
def _find_project_v8path():
|
||||||
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
|
d = os.getcwd()
|
||||||
|
while True:
|
||||||
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pf):
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
v = data.get("v8path")
|
||||||
|
if v:
|
||||||
|
return v
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return None
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
|
def _version_key(p):
|
||||||
|
"""Numeric sort key from version dir name."""
|
||||||
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_v8path(v8path):
|
||||||
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
|
if not v8path:
|
||||||
|
v8path = _find_project_v8path()
|
||||||
|
if not v8path:
|
||||||
|
if os.name == "nt":
|
||||||
|
candidates = (
|
||||||
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
|
if candidates:
|
||||||
|
v8path = max(candidates, key=_version_key)
|
||||||
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
|
else:
|
||||||
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if os.path.isdir(v8path):
|
||||||
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
|
if not os.path.isfile(v8path):
|
||||||
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Dump 1C information base to DT file",
|
||||||
|
allow_abbrev=False,
|
||||||
|
)
|
||||||
|
parser.add_argument("-V8Path", default="")
|
||||||
|
parser.add_argument("-InfoBasePath", default="")
|
||||||
|
parser.add_argument("-InfoBaseServer", default="")
|
||||||
|
parser.add_argument("-InfoBaseRef", default="")
|
||||||
|
parser.add_argument("-UserName", default="")
|
||||||
|
parser.add_argument("-Password", default="")
|
||||||
|
parser.add_argument("-OutputFile", required=True)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
|
# --- Validate connection ---
|
||||||
|
if engine == "ibcmd":
|
||||||
|
if not args.InfoBasePath:
|
||||||
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# --- Ensure output directory exists ---
|
||||||
|
out_dir = os.path.dirname(args.OutputFile)
|
||||||
|
if out_dir and not os.path.isdir(out_dir):
|
||||||
|
os.makedirs(out_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# --- ibcmd branch (file infobase only) ---
|
||||||
|
if engine == "ibcmd":
|
||||||
|
arguments = ["infobase", "dump", f"--db-path={args.InfoBasePath}"]
|
||||||
|
if args.UserName:
|
||||||
|
arguments.append(f"--user={args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
arguments.append(f"--password={args.Password}")
|
||||||
|
arguments.append(args.OutputFile)
|
||||||
|
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
||||||
|
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
||||||
|
arguments.append(f"--data={ib_data}")
|
||||||
|
print(f"Running: ibcmd {' '.join(arguments)}")
|
||||||
|
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||||
|
if result.returncode == 0:
|
||||||
|
print(f"Information base dumped successfully to: {args.OutputFile}")
|
||||||
|
else:
|
||||||
|
print(f"Error dumping information base (code: {result.returncode})", file=sys.stderr)
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
sys.exit(result.returncode)
|
||||||
|
|
||||||
|
# --- Temp dir ---
|
||||||
|
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_dt_{random.randint(0, 999999)}")
|
||||||
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# --- Build arguments ---
|
||||||
|
arguments = ["DESIGNER"]
|
||||||
|
|
||||||
|
if args.InfoBaseServer and args.InfoBaseRef:
|
||||||
|
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
|
||||||
|
else:
|
||||||
|
arguments.extend(["/F", args.InfoBasePath])
|
||||||
|
|
||||||
|
if args.UserName:
|
||||||
|
arguments.append(f"/N{args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
arguments.append(f"/P{args.Password}")
|
||||||
|
|
||||||
|
arguments.extend(["/DumpIB", args.OutputFile])
|
||||||
|
|
||||||
|
# --- Output ---
|
||||||
|
out_file = os.path.join(temp_dir, "dump_dt_log.txt")
|
||||||
|
arguments.extend(["/Out", out_file])
|
||||||
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
|
||||||
|
# --- Execute ---
|
||||||
|
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
||||||
|
result = subprocess.run(
|
||||||
|
[v8path] + arguments,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
exit_code = result.returncode
|
||||||
|
|
||||||
|
# --- Result ---
|
||||||
|
if exit_code == 0:
|
||||||
|
print(f"Information base dumped successfully to: {args.OutputFile}")
|
||||||
|
else:
|
||||||
|
print(f"Error dumping information base (code: {exit_code})", file=sys.stderr)
|
||||||
|
|
||||||
|
if os.path.isfile(out_file):
|
||||||
|
try:
|
||||||
|
with open(out_file, "r", encoding="utf-8-sig") as f:
|
||||||
|
log_content = f.read()
|
||||||
|
if log_content:
|
||||||
|
print("--- Log ---")
|
||||||
|
print(log_content)
|
||||||
|
print("--- End ---")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
sys.exit(exit_code)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if os.path.isdir(temp_dir):
|
||||||
|
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: db-dump-xml
|
name: db-dump-xml
|
||||||
description: Выгрузка конфигурации 1С в XML-файлы. Используй когда пользователь просит выгрузить конфигурацию в файлы, XML, исходники, DumpConfigToFiles
|
description: Выгрузка конфигурации 1С в XML-файлы. Используй когда нужно выгрузить конфигурацию в файлы, XML, исходники, DumpConfigToFiles
|
||||||
argument-hint: "[database] [outputDir]"
|
argument-hint: "[database] [outputDir]"
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash
|
- Bash
|
||||||
@@ -29,7 +29,7 @@ allowed-tools:
|
|||||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||||
4. Если ветка не совпала — используй `default`
|
4. Если ветка не совпала — используй `default`
|
||||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||||
Если файла нет — предложи `/db-list add`.
|
Если файла нет — предложи `/db-list add`.
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
Если в записи базы указан `configSrc` — используй как каталог выгрузки по умолчанию.
|
Если в записи базы указан `configSrc` — используй как каталог выгрузки по умолчанию.
|
||||||
@@ -37,14 +37,14 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-dump-xml/scripts/db-dump-xml.ps1 <параметры>
|
python ".opencode/skills/db-dump-xml/scripts/db-dump-xml.py" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
| Параметр | Обязательный | Описание |
|
||||||
|----------|:------------:|----------|
|
|----------|:------------:|----------|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
@@ -68,30 +68,23 @@ powershell.exe -NoProfile -File .claude/skills/db-dump-xml/scripts/db-dump-xml.p
|
|||||||
| `Partial` | Частичная — выбранные объекты из параметра `-Objects` |
|
| `Partial` | Частичная — выбранные объекты из параметра `-Objects` |
|
||||||
| `UpdateInfo` | Обновить только ConfigDumpInfo.xml без выгрузки файлов |
|
| `UpdateInfo` | Обновить только ConfigDumpInfo.xml без выгрузки файлов |
|
||||||
|
|
||||||
## Коды возврата
|
|
||||||
|
|
||||||
| Код | Описание |
|
|
||||||
|-----|----------|
|
|
||||||
| 0 | Успешно |
|
|
||||||
| 1 | Ошибка (см. лог) |
|
|
||||||
|
|
||||||
> Если пользователь просит выгрузить конкретные объекты — используй `-Mode Partial` с `-Objects`.
|
> Если пользователь просит выгрузить конкретные объекты — используй `-Mode Partial` с `-Objects`.
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Полная выгрузка (файловая база)
|
# Полная выгрузка (файловая база)
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-dump-xml/scripts/db-dump-xml.ps1 -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full
|
python ".opencode/skills/db-dump-xml/scripts/db-dump-xml.py" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full
|
||||||
|
|
||||||
# Инкрементальная выгрузка
|
# Инкрементальная выгрузка
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-dump-xml/scripts/db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Changes
|
python ".opencode/skills/db-dump-xml/scripts/db-dump-xml.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Changes
|
||||||
|
|
||||||
# Частичная выгрузка
|
# Частичная выгрузка
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-dump-xml/scripts/db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ"
|
python ".opencode/skills/db-dump-xml/scripts/db-dump-xml.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ"
|
||||||
|
|
||||||
# Серверная база
|
# Серверная база
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-dump-xml/scripts/db-dump-xml.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -ConfigDir "C:\WS\cfsrc" -Mode Full
|
python ".opencode/skills/db-dump-xml/scripts/db-dump-xml.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -ConfigDir "C:\WS\cfsrc" -Mode Full
|
||||||
|
|
||||||
# Выгрузка расширения
|
# Выгрузка расширения
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-dump-xml/scripts/db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
|
python ".opencode/skills/db-dump-xml/scripts/db-dump-xml.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
|
||||||
```
|
```
|
||||||
+105
-6
@@ -1,5 +1,6 @@
|
|||||||
# db-dump-xml v1.0 — Dump 1C configuration to XML files
|
# db-dump-xml v1.8 — Dump 1C configuration to XML files
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Выгрузка конфигурации 1С в XML-файлы
|
Выгрузка конфигурации 1С в XML-файлы
|
||||||
@@ -99,25 +100,85 @@ $OutputEncoding = [System.Text.Encoding]::UTF8
|
|||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
|
function Find-ProjectV8Path {
|
||||||
|
$dir = (Get-Location).Path
|
||||||
|
while ($dir) {
|
||||||
|
$pf = Join-Path $dir ".v8-project.json"
|
||||||
|
if (Test-Path $pf) {
|
||||||
|
try {
|
||||||
|
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
if ($j.v8path) { return [string]$j.v8path }
|
||||||
|
} catch {}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
$parent = Split-Path $dir -Parent
|
||||||
|
if (-not $parent -or $parent -eq $dir) { break }
|
||||||
|
$dir = $parent
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
if (-not $V8Path) {
|
if (-not $V8Path) {
|
||||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
$V8Path = Find-ProjectV8Path
|
||||||
|
}
|
||||||
|
if (-not $V8Path) {
|
||||||
|
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
||||||
|
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
||||||
|
Select-Object -First 1
|
||||||
if ($found) {
|
if ($found) {
|
||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
} elseif (Test-Path $V8Path -PathType Container) {
|
}
|
||||||
|
if (Test-Path $V8Path -PathType Container) {
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
|
function Invoke-IbcmdProcess {
|
||||||
|
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
||||||
|
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
||||||
|
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
||||||
|
param([string]$Exe, [string[]]$IbArgs)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
try {
|
||||||
|
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
} catch {}
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
$out = $p.StandardOutput.ReadToEnd()
|
||||||
|
$err = $p.StandardError.ReadToEnd()
|
||||||
|
$p.WaitForExit()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
if ($engine -eq "ibcmd") {
|
||||||
|
if (-not $InfoBasePath) {
|
||||||
|
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -139,6 +200,44 @@ $tempDir = Join-Path $env:TEMP "db_dump_xml_$(Get-Random)"
|
|||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
# --- ibcmd branch (file infobase only; hierarchical Full/Changes) ---
|
||||||
|
if ($Format -eq "Plain") {
|
||||||
|
Write-Host "Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($AllExtensions) {
|
||||||
|
$arguments = @("infobase", "config", "export", "all-extensions", "$ConfigDir", "--db-path=$InfoBasePath")
|
||||||
|
} elseif ($Mode -eq "UpdateInfo") {
|
||||||
|
Write-Host "Error: ibcmd config export does not support Mode UpdateInfo; use 1cv8" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
} elseif ($Mode -eq "Partial") {
|
||||||
|
$objList = @($Objects -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
|
||||||
|
$arguments = @("infobase", "config", "export", "objects") + $objList
|
||||||
|
$arguments += "--out=$ConfigDir", "--db-path=$InfoBasePath"
|
||||||
|
if ($Extension) { $arguments += "--extension=$Extension" }
|
||||||
|
} else {
|
||||||
|
$arguments = @("infobase", "config", "export", "--db-path=$InfoBasePath")
|
||||||
|
if ($Extension) { $arguments += "--extension=$Extension" }
|
||||||
|
$arguments += "$ConfigDir"
|
||||||
|
}
|
||||||
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
|
$arguments += "--data=$tempDir"
|
||||||
|
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
if ($exitCode -eq 0) {
|
||||||
|
Write-Host "Configuration exported successfully to: $ConfigDir" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "Error exporting configuration (code: $exitCode)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
if ($output) { Write-Host ($output | Out-String) }
|
||||||
|
exit $exitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1cv8 branch ---
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
$arguments = @("DESIGNER")
|
$arguments = @("DESIGNER")
|
||||||
|
|
||||||
@@ -0,0 +1,285 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# db-dump-xml v1.8 — Dump 1C configuration to XML files
|
||||||
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import atexit
|
||||||
|
import glob
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
|
def _find_project_v8path():
|
||||||
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
|
d = os.getcwd()
|
||||||
|
while True:
|
||||||
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pf):
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
v = data.get("v8path")
|
||||||
|
if v:
|
||||||
|
return v
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return None
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
|
def _version_key(p):
|
||||||
|
"""Numeric sort key from version dir name."""
|
||||||
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_v8path(v8path):
|
||||||
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
|
if not v8path:
|
||||||
|
v8path = _find_project_v8path()
|
||||||
|
if not v8path:
|
||||||
|
if os.name == "nt":
|
||||||
|
candidates = (
|
||||||
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
|
if candidates:
|
||||||
|
v8path = max(candidates, key=_version_key)
|
||||||
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
|
else:
|
||||||
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if os.path.isdir(v8path):
|
||||||
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
|
if not os.path.isfile(v8path):
|
||||||
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Dump 1C configuration to XML files",
|
||||||
|
allow_abbrev=False,
|
||||||
|
)
|
||||||
|
parser.add_argument("-V8Path", default="", help="Path to 1cv8.exe or its bin directory")
|
||||||
|
parser.add_argument("-InfoBasePath", default="", help="Path to file infobase")
|
||||||
|
parser.add_argument("-InfoBaseServer", default="", help="1C server (for server infobase)")
|
||||||
|
parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server")
|
||||||
|
parser.add_argument("-UserName", default="", help="1C user name")
|
||||||
|
parser.add_argument("-Password", default="", help="1C user password")
|
||||||
|
parser.add_argument("-ConfigDir", required=True, help="Directory for configuration dump")
|
||||||
|
parser.add_argument(
|
||||||
|
"-Mode",
|
||||||
|
default="Changes",
|
||||||
|
choices=["Full", "Changes", "Partial", "UpdateInfo"],
|
||||||
|
help="Dump mode (default: Changes)",
|
||||||
|
)
|
||||||
|
parser.add_argument("-Objects", default="", help="Comma-separated metadata object names (for Partial mode)")
|
||||||
|
parser.add_argument("-Extension", default="", help="Extension name to dump")
|
||||||
|
parser.add_argument("-AllExtensions", action="store_true", help="Dump all extensions")
|
||||||
|
parser.add_argument(
|
||||||
|
"-Format",
|
||||||
|
default="Hierarchical",
|
||||||
|
choices=["Hierarchical", "Plain"],
|
||||||
|
help="Dump format (default: Hierarchical)",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# --- Resolve V8Path ---
|
||||||
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
|
# --- Validate connection ---
|
||||||
|
if engine == "ibcmd":
|
||||||
|
if not args.InfoBasePath:
|
||||||
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# --- Validate Partial mode ---
|
||||||
|
if args.Mode == "Partial" and not args.Objects:
|
||||||
|
print("Error: -Objects required for Partial mode", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# --- Create output dir if needed ---
|
||||||
|
if not os.path.exists(args.ConfigDir):
|
||||||
|
os.makedirs(args.ConfigDir, exist_ok=True)
|
||||||
|
print(f"Created output directory: {args.ConfigDir}")
|
||||||
|
|
||||||
|
# --- ibcmd branch (file infobase only; hierarchical Full/Changes) ---
|
||||||
|
if engine == "ibcmd":
|
||||||
|
if args.Format == "Plain":
|
||||||
|
print("Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if args.AllExtensions:
|
||||||
|
arguments = ["infobase", "config", "export", "all-extensions", args.ConfigDir, f"--db-path={args.InfoBasePath}"]
|
||||||
|
elif args.Mode == "UpdateInfo":
|
||||||
|
print("Error: ibcmd config export does not support Mode UpdateInfo; use 1cv8", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
elif args.Mode == "Partial":
|
||||||
|
obj_list = [o.strip() for o in args.Objects.split(",") if o.strip()]
|
||||||
|
arguments = ["infobase", "config", "export", "objects"] + obj_list
|
||||||
|
arguments += [f"--out={args.ConfigDir}", f"--db-path={args.InfoBasePath}"]
|
||||||
|
if args.Extension:
|
||||||
|
arguments.append(f"--extension={args.Extension}")
|
||||||
|
else:
|
||||||
|
arguments = ["infobase", "config", "export", f"--db-path={args.InfoBasePath}"]
|
||||||
|
if args.Extension:
|
||||||
|
arguments.append(f"--extension={args.Extension}")
|
||||||
|
arguments.append(args.ConfigDir)
|
||||||
|
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
||||||
|
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
||||||
|
if args.UserName:
|
||||||
|
arguments.append(f"--user={args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
arguments.append(f"--password={args.Password}")
|
||||||
|
arguments.append(f"--data={ib_data}")
|
||||||
|
print(f"Running: ibcmd {' '.join(arguments)}")
|
||||||
|
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||||
|
if result.returncode == 0:
|
||||||
|
print(f"Configuration exported successfully to: {args.ConfigDir}")
|
||||||
|
else:
|
||||||
|
print(f"Error exporting configuration (code: {result.returncode})", file=sys.stderr)
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
sys.exit(result.returncode)
|
||||||
|
|
||||||
|
# --- Temp dir ---
|
||||||
|
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_xml_{random.randint(0, 999999)}")
|
||||||
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# --- Build arguments ---
|
||||||
|
arguments = ["DESIGNER"]
|
||||||
|
|
||||||
|
if args.InfoBaseServer and args.InfoBaseRef:
|
||||||
|
arguments += ["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"]
|
||||||
|
else:
|
||||||
|
arguments += ["/F", args.InfoBasePath]
|
||||||
|
|
||||||
|
if args.UserName:
|
||||||
|
arguments.append(f"/N{args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
arguments.append(f"/P{args.Password}")
|
||||||
|
|
||||||
|
arguments += ["/DumpConfigToFiles", args.ConfigDir]
|
||||||
|
arguments += ["-Format", args.Format]
|
||||||
|
|
||||||
|
if args.Mode == "Full":
|
||||||
|
print("Executing full configuration dump...")
|
||||||
|
elif args.Mode == "Changes":
|
||||||
|
print("Executing incremental configuration dump...")
|
||||||
|
arguments.append("-update")
|
||||||
|
arguments.append("-force")
|
||||||
|
elif args.Mode == "Partial":
|
||||||
|
print("Executing partial configuration dump...")
|
||||||
|
object_list = [obj.strip() for obj in args.Objects.split(",") if obj.strip()]
|
||||||
|
|
||||||
|
list_file = os.path.join(temp_dir, "dump_list.txt")
|
||||||
|
with open(list_file, "w", encoding="utf-8-sig") as f:
|
||||||
|
f.write("\n".join(object_list))
|
||||||
|
|
||||||
|
arguments += ["-listFile", list_file]
|
||||||
|
print(f"Objects to dump: {len(object_list)}")
|
||||||
|
for obj in object_list:
|
||||||
|
print(f" {obj}")
|
||||||
|
elif args.Mode == "UpdateInfo":
|
||||||
|
print("Updating ConfigDumpInfo.xml...")
|
||||||
|
arguments.append("-configDumpInfoOnly")
|
||||||
|
|
||||||
|
# --- Extensions ---
|
||||||
|
if args.Extension:
|
||||||
|
arguments += ["-Extension", args.Extension]
|
||||||
|
elif args.AllExtensions:
|
||||||
|
arguments.append("-AllExtensions")
|
||||||
|
|
||||||
|
# --- Output ---
|
||||||
|
out_file = os.path.join(temp_dir, "dump_log.txt")
|
||||||
|
arguments += ["/Out", out_file]
|
||||||
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
|
||||||
|
# --- Execute ---
|
||||||
|
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
||||||
|
result = subprocess.run(
|
||||||
|
[v8path] + arguments,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
exit_code = result.returncode
|
||||||
|
|
||||||
|
# --- Result ---
|
||||||
|
if exit_code == 0:
|
||||||
|
print("Dump completed successfully")
|
||||||
|
print(f"Configuration dumped to: {args.ConfigDir}")
|
||||||
|
else:
|
||||||
|
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
|
||||||
|
|
||||||
|
if os.path.isfile(out_file):
|
||||||
|
try:
|
||||||
|
with open(out_file, "r", encoding="utf-8-sig") as f:
|
||||||
|
log_content = f.read()
|
||||||
|
if log_content:
|
||||||
|
print("--- Log ---")
|
||||||
|
print(log_content)
|
||||||
|
print("--- End ---")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
sys.exit(exit_code)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if os.path.exists(temp_dir):
|
||||||
|
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: db-list
|
name: db-list
|
||||||
description: Управление реестром баз данных 1С (.v8-project.json). Используй когда пользователь говорит про базы данных, список баз, "добавь базу", "какие базы есть"
|
description: Управление реестром баз данных 1С (.v8-project.json). Используй когда нужно работать с реестром баз — список баз, зарегистрировать базу в реестре, какие базы есть
|
||||||
argument-hint: "[add|remove|show]"
|
argument-hint: "[add|remove|show]"
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Read
|
- Read
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: db-load-cf
|
name: db-load-cf
|
||||||
description: Загрузка конфигурации 1С из CF-файла. Используй когда пользователь просит загрузить конфигурацию из CF, восстановить из бэкапа CF
|
description: Загрузка конфигурации 1С из CF-файла. Используй когда нужно загрузить конфигурацию из CF, восстановить из бэкапа CF
|
||||||
argument-hint: <input.cf> [database]
|
argument-hint: <input.cf> [database]
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash
|
- Bash
|
||||||
@@ -29,21 +29,21 @@ allowed-tools:
|
|||||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||||
4. Если ветка не совпала — используй `default`
|
4. Если ветка не совпала — используй `default`
|
||||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||||
Если файла нет — предложи `/db-list add`.
|
Если файла нет — предложи `/db-list add`.
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-load-cf/scripts/db-load-cf.ps1 <параметры>
|
python ".opencode/skills/db-load-cf/scripts/db-load-cf.py" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
| Параметр | Обязательный | Описание |
|
||||||
|----------|:------------:|----------|
|
|----------|:------------:|----------|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
@@ -55,27 +55,19 @@ powershell.exe -NoProfile -File .claude/skills/db-load-cf/scripts/db-load-cf.ps1
|
|||||||
|
|
||||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||||
|
|
||||||
## Коды возврата
|
|
||||||
|
|
||||||
| Код | Описание |
|
|
||||||
|-----|----------|
|
|
||||||
| 0 | Успешно |
|
|
||||||
| 1 | Ошибка (см. лог) |
|
|
||||||
|
|
||||||
## После выполнения
|
## После выполнения
|
||||||
|
|
||||||
1. Прочитай лог-файл и покажи результат
|
**Предложи выполнить `/db-update`** — загрузка CF обновляет только «основную» конфигурацию конфигуратора, для применения к БД нужен `/UpdateDBCfg`
|
||||||
2. **Предложи выполнить `/db-update`** — загрузка CF обновляет только «основную» конфигурацию конфигуратора, для применения к БД нужен `/UpdateDBCfg`
|
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Файловая база
|
# Файловая база
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-load-cf/scripts/db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\config.cf"
|
python ".opencode/skills/db-load-cf/scripts/db-load-cf.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\config.cf"
|
||||||
|
|
||||||
# Серверная база
|
# Серверная база
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-load-cf/scripts/db-load-cf.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "config.cf"
|
python ".opencode/skills/db-load-cf/scripts/db-load-cf.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "config.cf"
|
||||||
|
|
||||||
# Загрузка расширения
|
# Загрузка расширения
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-load-cf/scripts/db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "ext.cfe" -Extension "МоёРасширение"
|
python ".opencode/skills/db-load-cf/scripts/db-load-cf.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "ext.cfe" -Extension "МоёРасширение"
|
||||||
```
|
```
|
||||||
+93
-6
@@ -1,5 +1,6 @@
|
|||||||
# db-load-cf v1.0 — Load 1C configuration from CF file
|
# db-load-cf v1.6 — Load 1C configuration from CF file
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Загрузка конфигурации 1С из CF-файла
|
Загрузка конфигурации 1С из CF-файла
|
||||||
@@ -76,25 +77,85 @@ $OutputEncoding = [System.Text.Encoding]::UTF8
|
|||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
|
function Find-ProjectV8Path {
|
||||||
|
$dir = (Get-Location).Path
|
||||||
|
while ($dir) {
|
||||||
|
$pf = Join-Path $dir ".v8-project.json"
|
||||||
|
if (Test-Path $pf) {
|
||||||
|
try {
|
||||||
|
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
if ($j.v8path) { return [string]$j.v8path }
|
||||||
|
} catch {}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
$parent = Split-Path $dir -Parent
|
||||||
|
if (-not $parent -or $parent -eq $dir) { break }
|
||||||
|
$dir = $parent
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
if (-not $V8Path) {
|
if (-not $V8Path) {
|
||||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
$V8Path = Find-ProjectV8Path
|
||||||
|
}
|
||||||
|
if (-not $V8Path) {
|
||||||
|
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
||||||
|
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
||||||
|
Select-Object -First 1
|
||||||
if ($found) {
|
if ($found) {
|
||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
} elseif (Test-Path $V8Path -PathType Container) {
|
}
|
||||||
|
if (Test-Path $V8Path -PathType Container) {
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
|
function Invoke-IbcmdProcess {
|
||||||
|
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
||||||
|
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
||||||
|
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
||||||
|
param([string]$Exe, [string[]]$IbArgs)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
try {
|
||||||
|
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
} catch {}
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
$out = $p.StandardOutput.ReadToEnd()
|
||||||
|
$err = $p.StandardError.ReadToEnd()
|
||||||
|
$p.WaitForExit()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
if ($engine -eq "ibcmd") {
|
||||||
|
if (-not $InfoBasePath) {
|
||||||
|
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -110,6 +171,32 @@ $tempDir = Join-Path $env:TEMP "db_load_cf_$(Get-Random)"
|
|||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
# --- ibcmd branch (file infobase only) ---
|
||||||
|
if ($AllExtensions) {
|
||||||
|
Write-Host "Error: ibcmd config load does not support -AllExtensions (use -Extension)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
$arguments = @("infobase", "config", "load", "--db-path=$InfoBasePath")
|
||||||
|
if ($Extension) { $arguments += "--extension=$Extension" }
|
||||||
|
$arguments += "$InputFile"
|
||||||
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
|
$arguments += "--data=$tempDir"
|
||||||
|
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
if ($exitCode -eq 0) {
|
||||||
|
Write-Host "Configuration loaded successfully from: $InputFile" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
if ($output) { Write-Host ($output | Out-String) }
|
||||||
|
exit $exitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1cv8 branch ---
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
$arguments = @("DESIGNER")
|
$arguments = @("DESIGNER")
|
||||||
|
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# db-load-cf v1.6 — Load 1C configuration from CF file
|
||||||
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import atexit
|
||||||
|
import glob
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
|
def _find_project_v8path():
|
||||||
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
|
d = os.getcwd()
|
||||||
|
while True:
|
||||||
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pf):
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
v = data.get("v8path")
|
||||||
|
if v:
|
||||||
|
return v
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return None
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
|
def _version_key(p):
|
||||||
|
"""Numeric sort key from version dir name."""
|
||||||
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_v8path(v8path):
|
||||||
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
|
if not v8path:
|
||||||
|
v8path = _find_project_v8path()
|
||||||
|
if not v8path:
|
||||||
|
if os.name == "nt":
|
||||||
|
candidates = (
|
||||||
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
|
if candidates:
|
||||||
|
v8path = max(candidates, key=_version_key)
|
||||||
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
|
else:
|
||||||
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if os.path.isdir(v8path):
|
||||||
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
|
if not os.path.isfile(v8path):
|
||||||
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Load 1C configuration from CF file",
|
||||||
|
allow_abbrev=False,
|
||||||
|
)
|
||||||
|
parser.add_argument("-V8Path", default="")
|
||||||
|
parser.add_argument("-InfoBasePath", default="")
|
||||||
|
parser.add_argument("-InfoBaseServer", default="")
|
||||||
|
parser.add_argument("-InfoBaseRef", default="")
|
||||||
|
parser.add_argument("-UserName", default="")
|
||||||
|
parser.add_argument("-Password", default="")
|
||||||
|
parser.add_argument("-InputFile", required=True)
|
||||||
|
parser.add_argument("-Extension", default="")
|
||||||
|
parser.add_argument("-AllExtensions", action="store_true")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
|
# --- Validate connection ---
|
||||||
|
if engine == "ibcmd":
|
||||||
|
if not args.InfoBasePath:
|
||||||
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# --- Validate input file ---
|
||||||
|
if not os.path.isfile(args.InputFile):
|
||||||
|
print(f"Error: input file not found: {args.InputFile}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# --- ibcmd branch (file infobase only) ---
|
||||||
|
if engine == "ibcmd":
|
||||||
|
if args.AllExtensions:
|
||||||
|
print("Error: ibcmd config load does not support -AllExtensions (use -Extension)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
arguments = ["infobase", "config", "load", f"--db-path={args.InfoBasePath}"]
|
||||||
|
if args.Extension:
|
||||||
|
arguments.append(f"--extension={args.Extension}")
|
||||||
|
arguments.append(args.InputFile)
|
||||||
|
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
||||||
|
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
||||||
|
if args.UserName:
|
||||||
|
arguments.append(f"--user={args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
arguments.append(f"--password={args.Password}")
|
||||||
|
arguments.append(f"--data={ib_data}")
|
||||||
|
print(f"Running: ibcmd {' '.join(arguments)}")
|
||||||
|
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||||
|
if result.returncode == 0:
|
||||||
|
print(f"Configuration loaded successfully from: {args.InputFile}")
|
||||||
|
else:
|
||||||
|
print(f"Error loading configuration (code: {result.returncode})", file=sys.stderr)
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
sys.exit(result.returncode)
|
||||||
|
|
||||||
|
# --- Temp dir ---
|
||||||
|
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_cf_{random.randint(0, 999999)}")
|
||||||
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# --- Build arguments ---
|
||||||
|
arguments = ["DESIGNER"]
|
||||||
|
|
||||||
|
if args.InfoBaseServer and args.InfoBaseRef:
|
||||||
|
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
|
||||||
|
else:
|
||||||
|
arguments.extend(["/F", args.InfoBasePath])
|
||||||
|
|
||||||
|
if args.UserName:
|
||||||
|
arguments.append(f"/N{args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
arguments.append(f"/P{args.Password}")
|
||||||
|
|
||||||
|
arguments.extend(["/LoadCfg", args.InputFile])
|
||||||
|
|
||||||
|
# --- Extensions ---
|
||||||
|
if args.Extension:
|
||||||
|
arguments.extend(["-Extension", args.Extension])
|
||||||
|
elif args.AllExtensions:
|
||||||
|
arguments.append("-AllExtensions")
|
||||||
|
|
||||||
|
# --- Output ---
|
||||||
|
out_file = os.path.join(temp_dir, "load_cf_log.txt")
|
||||||
|
arguments.extend(["/Out", out_file])
|
||||||
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
|
||||||
|
# --- Execute ---
|
||||||
|
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
||||||
|
result = subprocess.run(
|
||||||
|
[v8path] + arguments,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
exit_code = result.returncode
|
||||||
|
|
||||||
|
# --- Result ---
|
||||||
|
if exit_code == 0:
|
||||||
|
print(f"Configuration loaded successfully from: {args.InputFile}")
|
||||||
|
else:
|
||||||
|
print(f"Error loading configuration (code: {exit_code})", file=sys.stderr)
|
||||||
|
|
||||||
|
if os.path.isfile(out_file):
|
||||||
|
try:
|
||||||
|
with open(out_file, "r", encoding="utf-8-sig") as f:
|
||||||
|
log_content = f.read()
|
||||||
|
if log_content:
|
||||||
|
print("--- Log ---")
|
||||||
|
print(log_content)
|
||||||
|
print("--- End ---")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
sys.exit(exit_code)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if os.path.isdir(temp_dir):
|
||||||
|
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
---
|
||||||
|
name: db-load-dt
|
||||||
|
description: Загрузка информационной базы 1С из DT-файла — полная перезапись базы (конфигурация + данные). Используй когда нужно загрузить архив информационной базы, восстановить базу, загрузить dt
|
||||||
|
disable-model-invocation: true
|
||||||
|
argument-hint: <input.dt> [database]
|
||||||
|
allowed-tools:
|
||||||
|
- Bash
|
||||||
|
- Read
|
||||||
|
- Glob
|
||||||
|
- AskUserQuestion
|
||||||
|
---
|
||||||
|
|
||||||
|
# /db-load-dt — Загрузка информационной базы из DT-файла
|
||||||
|
|
||||||
|
Восстанавливает информационную базу целиком (конфигурация **+ данные**) из DT-файла.
|
||||||
|
|
||||||
|
> ⚠️ **Необратимая операция.** Загрузка `.dt` **полностью перезаписывает базу** — и
|
||||||
|
> конфигурацию, и все данные. Текущее содержимое базы будет потеряно. После загрузки
|
||||||
|
> `/db-update` **не нужен** — конфигурация БД уже синхронна внутри снимка.
|
||||||
|
|
||||||
|
## Когда НЕ использовать
|
||||||
|
|
||||||
|
- Нужно создать **новую** базу из `.dt` → используй `/db-create` (из DT-шаблона), а не загрузку
|
||||||
|
в существующую.
|
||||||
|
- Нужно обновить только конфигурацию (без данных) → `/db-load-cf` или `/db-load-xml`.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
/db-load-dt <input.dt> [database]
|
||||||
|
/db-load-dt backup.dt dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Порядок действий перед загрузкой
|
||||||
|
|
||||||
|
1. Предложи пользователю сначала сделать `/db-dump-dt` текущего состояния базы — это точка
|
||||||
|
отката (восстановиться будет нечем, если не сохранить).
|
||||||
|
2. Запроси **явное подтверждение**: вся база (данные + конфигурация) будет перезаписана.
|
||||||
|
3. Только после подтверждения выполняй загрузку.
|
||||||
|
|
||||||
|
## Параметры подключения
|
||||||
|
|
||||||
|
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу:
|
||||||
|
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
|
||||||
|
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||||
|
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||||
|
4. Если ветка не совпала — используй `default`
|
||||||
|
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||||
|
Если файла нет — предложи `/db-list add`.
|
||||||
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
|
|
||||||
|
## Команда
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python ".opencode/skills/db-load-dt/scripts/db-load-dt.py" <параметры>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Параметры скрипта
|
||||||
|
|
||||||
|
| Параметр | Обязательный | Описание |
|
||||||
|
|----------|:------------:|----------|
|
||||||
|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||||
|
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||||
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
|
| `-UserName <имя>` | нет | Имя пользователя |
|
||||||
|
| `-Password <пароль>` | нет | Пароль |
|
||||||
|
| `-InputFile <путь>` | да | Путь к DT-файлу |
|
||||||
|
| `-JobsCount <N>` | нет | Число фоновых заданий загрузки (0 = по числу процессоров) |
|
||||||
|
| `-UnlockCode <код>` | нет | Код разблокировки (`/UC`), если заблокировано начало сеансов |
|
||||||
|
|
||||||
|
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||||
|
|
||||||
|
## После выполнения
|
||||||
|
|
||||||
|
Если база занята (активные сеансы), загрузка не выполнится — для серверной базы можно
|
||||||
|
передать `-UnlockCode`; иначе освободи базу и повтори.
|
||||||
|
|
||||||
|
## Примеры
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Файловая база
|
||||||
|
python ".opencode/skills/db-load-dt/scripts/db-load-dt.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\base.dt"
|
||||||
|
|
||||||
|
# Серверная база с ускорением загрузки
|
||||||
|
python ".opencode/skills/db-load-dt/scripts/db-load-dt.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "base.dt" -JobsCount 4
|
||||||
|
```
|
||||||
|
|
||||||
|
## Связанные навыки
|
||||||
|
|
||||||
|
- `/db-dump-dt` — выгрузка ИБ в DT (обратная операция, точка отката перед загрузкой)
|
||||||
|
- `/db-create` — создать новую базу (в т.ч. из DT-шаблона)
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
# db-load-dt v1.5 — Load 1C information base from DT file
|
||||||
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Загрузка информационной базы 1С из DT-файла
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Загружает информационную базу целиком (конфигурация + данные) из DT-файла.
|
||||||
|
ВНИМАНИЕ: операция полностью перезаписывает базу.
|
||||||
|
|
||||||
|
.PARAMETER V8Path
|
||||||
|
Путь к каталогу bin платформы или к 1cv8.exe
|
||||||
|
|
||||||
|
.PARAMETER InfoBasePath
|
||||||
|
Путь к файловой информационной базе
|
||||||
|
|
||||||
|
.PARAMETER InfoBaseServer
|
||||||
|
Сервер 1С (для серверной базы)
|
||||||
|
|
||||||
|
.PARAMETER InfoBaseRef
|
||||||
|
Имя базы на сервере
|
||||||
|
|
||||||
|
.PARAMETER UserName
|
||||||
|
Имя пользователя 1С
|
||||||
|
|
||||||
|
.PARAMETER Password
|
||||||
|
Пароль пользователя
|
||||||
|
|
||||||
|
.PARAMETER InputFile
|
||||||
|
Путь к DT-файлу для загрузки
|
||||||
|
|
||||||
|
.PARAMETER JobsCount
|
||||||
|
Количество фоновых заданий для загрузки (0 = по числу процессоров)
|
||||||
|
|
||||||
|
.PARAMETER UnlockCode
|
||||||
|
Код разблокировки базы (/UC) — если заблокировано начало сеансов
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\db-load-dt.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "backup.dt"
|
||||||
|
#>
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string]$V8Path,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string]$InfoBasePath,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string]$InfoBaseServer,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string]$InfoBaseRef,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string]$UserName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string]$Password,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$true)]
|
||||||
|
[string]$InputFile,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[int]$JobsCount = 0,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string]$UnlockCode
|
||||||
|
)
|
||||||
|
|
||||||
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
# --- Resolve V8Path ---
|
||||||
|
function Find-ProjectV8Path {
|
||||||
|
$dir = (Get-Location).Path
|
||||||
|
while ($dir) {
|
||||||
|
$pf = Join-Path $dir ".v8-project.json"
|
||||||
|
if (Test-Path $pf) {
|
||||||
|
try {
|
||||||
|
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
if ($j.v8path) { return [string]$j.v8path }
|
||||||
|
} catch {}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
$parent = Split-Path $dir -Parent
|
||||||
|
if (-not $parent -or $parent -eq $dir) { break }
|
||||||
|
$dir = $parent
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $V8Path) {
|
||||||
|
$V8Path = Find-ProjectV8Path
|
||||||
|
}
|
||||||
|
if (-not $V8Path) {
|
||||||
|
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
||||||
|
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
||||||
|
Select-Object -First 1
|
||||||
|
if ($found) {
|
||||||
|
$V8Path = $found.FullName
|
||||||
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
|
} else {
|
||||||
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Test-Path $V8Path -PathType Container) {
|
||||||
|
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path $V8Path)) {
|
||||||
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
|
function Invoke-IbcmdProcess {
|
||||||
|
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
||||||
|
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
||||||
|
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
||||||
|
param([string]$Exe, [string[]]$IbArgs)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
try {
|
||||||
|
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
} catch {}
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
$out = $p.StandardOutput.ReadToEnd()
|
||||||
|
$err = $p.StandardError.ReadToEnd()
|
||||||
|
$p.WaitForExit()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
|
# --- Validate connection ---
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
if (-not $InfoBasePath) {
|
||||||
|
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||||
|
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Validate input file ---
|
||||||
|
if (-not (Test-Path $InputFile)) {
|
||||||
|
Write-Host "Error: input file not found: $InputFile" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Temp dir ---
|
||||||
|
$tempDir = Join-Path $env:TEMP "db_load_dt_$(Get-Random)"
|
||||||
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
# --- ibcmd branch (file infobase only) ---
|
||||||
|
$arguments = @("infobase", "restore", "--db-path=$InfoBasePath")
|
||||||
|
if (-not (Test-Path (Join-Path $InfoBasePath "1Cv8.1CD"))) { $arguments += "--create-database" }
|
||||||
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
|
$arguments += "$InputFile"
|
||||||
|
|
||||||
|
$arguments += "--data=$tempDir"
|
||||||
|
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
if ($exitCode -eq 0) {
|
||||||
|
Write-Host "Information base restored successfully from: $InputFile" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "Error restoring information base (code: $exitCode)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
if ($output) { Write-Host ($output | Out-String) }
|
||||||
|
exit $exitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1cv8 branch ---
|
||||||
|
# --- Build arguments ---
|
||||||
|
$arguments = @("DESIGNER")
|
||||||
|
|
||||||
|
if ($InfoBaseServer -and $InfoBaseRef) {
|
||||||
|
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
|
||||||
|
} else {
|
||||||
|
$arguments += "/F", "`"$InfoBasePath`""
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($UserName) { $arguments += "/N`"$UserName`"" }
|
||||||
|
if ($Password) { $arguments += "/P`"$Password`"" }
|
||||||
|
if ($UnlockCode) { $arguments += "/UC`"$UnlockCode`"" }
|
||||||
|
|
||||||
|
$arguments += "/RestoreIB", "`"$InputFile`""
|
||||||
|
if ($JobsCount -gt 0) { $arguments += "-JobsCount", "$JobsCount" }
|
||||||
|
|
||||||
|
# --- Output ---
|
||||||
|
$outFile = Join-Path $tempDir "load_dt_log.txt"
|
||||||
|
$arguments += "/Out", "`"$outFile`""
|
||||||
|
$arguments += "/DisableStartupDialogs"
|
||||||
|
|
||||||
|
# --- Execute ---
|
||||||
|
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
||||||
|
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
||||||
|
$exitCode = $process.ExitCode
|
||||||
|
|
||||||
|
# --- Result ---
|
||||||
|
if ($exitCode -eq 0) {
|
||||||
|
Write-Host "Information base restored successfully from: $InputFile" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "Error restoring information base (code: $exitCode)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Path $outFile) {
|
||||||
|
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
|
||||||
|
if ($logContent) {
|
||||||
|
Write-Host "--- Log ---"
|
||||||
|
Write-Host $logContent
|
||||||
|
Write-Host "--- End ---"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exit $exitCode
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
if (Test-Path $tempDir) {
|
||||||
|
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# db-load-dt v1.5 — Load 1C information base from DT file
|
||||||
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import atexit
|
||||||
|
import glob
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
|
def _find_project_v8path():
|
||||||
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
|
d = os.getcwd()
|
||||||
|
while True:
|
||||||
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pf):
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
v = data.get("v8path")
|
||||||
|
if v:
|
||||||
|
return v
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return None
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
|
def _version_key(p):
|
||||||
|
"""Numeric sort key from version dir name."""
|
||||||
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_v8path(v8path):
|
||||||
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
|
if not v8path:
|
||||||
|
v8path = _find_project_v8path()
|
||||||
|
if not v8path:
|
||||||
|
if os.name == "nt":
|
||||||
|
candidates = (
|
||||||
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
|
if candidates:
|
||||||
|
v8path = max(candidates, key=_version_key)
|
||||||
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
|
else:
|
||||||
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if os.path.isdir(v8path):
|
||||||
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
|
if not os.path.isfile(v8path):
|
||||||
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Load 1C information base from DT file",
|
||||||
|
allow_abbrev=False,
|
||||||
|
)
|
||||||
|
parser.add_argument("-V8Path", default="")
|
||||||
|
parser.add_argument("-InfoBasePath", default="")
|
||||||
|
parser.add_argument("-InfoBaseServer", default="")
|
||||||
|
parser.add_argument("-InfoBaseRef", default="")
|
||||||
|
parser.add_argument("-UserName", default="")
|
||||||
|
parser.add_argument("-Password", default="")
|
||||||
|
parser.add_argument("-InputFile", required=True)
|
||||||
|
parser.add_argument("-JobsCount", type=int, default=0)
|
||||||
|
parser.add_argument("-UnlockCode", default="")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
|
# --- Validate connection ---
|
||||||
|
if engine == "ibcmd":
|
||||||
|
if not args.InfoBasePath:
|
||||||
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# --- Validate input file ---
|
||||||
|
if not os.path.isfile(args.InputFile):
|
||||||
|
print(f"Error: input file not found: {args.InputFile}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# --- ibcmd branch (file infobase only) ---
|
||||||
|
if engine == "ibcmd":
|
||||||
|
arguments = ["infobase", "restore", f"--db-path={args.InfoBasePath}"]
|
||||||
|
if not os.path.isfile(os.path.join(args.InfoBasePath, "1Cv8.1CD")):
|
||||||
|
arguments.append("--create-database")
|
||||||
|
if args.UserName:
|
||||||
|
arguments.append(f"--user={args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
arguments.append(f"--password={args.Password}")
|
||||||
|
arguments.append(args.InputFile)
|
||||||
|
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
||||||
|
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
||||||
|
arguments.append(f"--data={ib_data}")
|
||||||
|
print(f"Running: ibcmd {' '.join(arguments)}")
|
||||||
|
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||||
|
if result.returncode == 0:
|
||||||
|
print(f"Information base restored successfully from: {args.InputFile}")
|
||||||
|
else:
|
||||||
|
print(f"Error restoring information base (code: {result.returncode})", file=sys.stderr)
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
sys.exit(result.returncode)
|
||||||
|
|
||||||
|
# --- Temp dir ---
|
||||||
|
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_dt_{random.randint(0, 999999)}")
|
||||||
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# --- Build arguments ---
|
||||||
|
arguments = ["DESIGNER"]
|
||||||
|
|
||||||
|
if args.InfoBaseServer and args.InfoBaseRef:
|
||||||
|
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
|
||||||
|
else:
|
||||||
|
arguments.extend(["/F", args.InfoBasePath])
|
||||||
|
|
||||||
|
if args.UserName:
|
||||||
|
arguments.append(f"/N{args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
arguments.append(f"/P{args.Password}")
|
||||||
|
if args.UnlockCode:
|
||||||
|
arguments.append(f"/UC{args.UnlockCode}")
|
||||||
|
|
||||||
|
arguments.extend(["/RestoreIB", args.InputFile])
|
||||||
|
if args.JobsCount > 0:
|
||||||
|
arguments.extend(["-JobsCount", str(args.JobsCount)])
|
||||||
|
|
||||||
|
# --- Output ---
|
||||||
|
out_file = os.path.join(temp_dir, "load_dt_log.txt")
|
||||||
|
arguments.extend(["/Out", out_file])
|
||||||
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
|
||||||
|
# --- Execute ---
|
||||||
|
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
||||||
|
result = subprocess.run(
|
||||||
|
[v8path] + arguments,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
exit_code = result.returncode
|
||||||
|
|
||||||
|
# --- Result ---
|
||||||
|
if exit_code == 0:
|
||||||
|
print(f"Information base restored successfully from: {args.InputFile}")
|
||||||
|
else:
|
||||||
|
print(f"Error restoring information base (code: {exit_code})", file=sys.stderr)
|
||||||
|
|
||||||
|
if os.path.isfile(out_file):
|
||||||
|
try:
|
||||||
|
with open(out_file, "r", encoding="utf-8-sig") as f:
|
||||||
|
log_content = f.read()
|
||||||
|
if log_content:
|
||||||
|
print("--- Log ---")
|
||||||
|
print(log_content)
|
||||||
|
print("--- End ---")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
sys.exit(exit_code)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if os.path.isdir(temp_dir):
|
||||||
|
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: db-load-git
|
name: db-load-git
|
||||||
description: Загрузка изменений из Git в базу 1С. Используй когда пользователь просит загрузить изменения из гита, обновить базу из репозитория, partial load из коммита
|
description: Загрузка изменений из Git в базу 1С. Используй когда нужно загрузить изменения из гита, обновить базу из репозитория, partial load из коммита
|
||||||
argument-hint: "[database] [source]"
|
argument-hint: "[database] [source]"
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash
|
- Bash
|
||||||
@@ -30,7 +30,7 @@ allowed-tools:
|
|||||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||||
4. Если ветка не совпала — используй `default`
|
4. Если ветка не совпала — используй `default`
|
||||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||||
Если файла нет — предложи `/db-list add`.
|
Если файла нет — предложи `/db-list add`.
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
Если в записи базы указан `configSrc` — используй как каталог конфигурации.
|
Если в записи базы указан `configSrc` — используй как каталог конфигурации.
|
||||||
@@ -38,14 +38,14 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-load-git/scripts/db-load-git.ps1 <параметры>
|
python ".opencode/skills/db-load-git/scripts/db-load-git.py" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
| Параметр | Обязательный | Описание |
|
||||||
|----------|:------------:|----------|
|
|----------|:------------:|----------|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
@@ -64,15 +64,14 @@ powershell.exe -NoProfile -File .claude/skills/db-load-git/scripts/db-load-git.p
|
|||||||
|
|
||||||
## После выполнения
|
## После выполнения
|
||||||
|
|
||||||
1. Показать список загруженных файлов и результат из лога
|
Если `-UpdateDB` не был указан — **предложить `/db-update`** для применения изменений к БД
|
||||||
2. Если `-UpdateDB` не был указан — **предложить `/db-update`** для применения изменений к БД
|
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Все незафиксированные изменения
|
# Все незафиксированные изменения
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-load-git/scripts/db-load-git.ps1 -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source All -UpdateDB
|
python ".opencode/skills/db-load-git/scripts/db-load-git.py" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source All -UpdateDB
|
||||||
|
|
||||||
# Из диапазона коммитов
|
# Из диапазона коммитов
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-load-git/scripts/db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source Commit -CommitRange "HEAD~3..HEAD"
|
python ".opencode/skills/db-load-git/scripts/db-load-git.py" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source Commit -CommitRange "HEAD~3..HEAD"
|
||||||
```
|
```
|
||||||
+141
-23
@@ -1,5 +1,6 @@
|
|||||||
# db-load-git v1.3 — Load Git changes into 1C database
|
# db-load-git v1.11 — Load Git changes into 1C database
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Загрузка изменений из Git в базу 1С
|
Загрузка изменений из Git в базу 1С
|
||||||
@@ -120,27 +121,86 @@ function Get-ObjectXmlFromSubFile {
|
|||||||
|
|
||||||
# --- Resolve V8Path (skip if DryRun) ---
|
# --- Resolve V8Path (skip if DryRun) ---
|
||||||
if (-not $DryRun) {
|
if (-not $DryRun) {
|
||||||
|
function Find-ProjectV8Path {
|
||||||
|
$dir = (Get-Location).Path
|
||||||
|
while ($dir) {
|
||||||
|
$pf = Join-Path $dir ".v8-project.json"
|
||||||
|
if (Test-Path $pf) {
|
||||||
|
try {
|
||||||
|
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
if ($j.v8path) { return [string]$j.v8path }
|
||||||
|
} catch {}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
$parent = Split-Path $dir -Parent
|
||||||
|
if (-not $parent -or $parent -eq $dir) { break }
|
||||||
|
$dir = $parent
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
if (-not $V8Path) {
|
if (-not $V8Path) {
|
||||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
$V8Path = Find-ProjectV8Path
|
||||||
|
}
|
||||||
|
if (-not $V8Path) {
|
||||||
|
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
||||||
|
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
||||||
|
Select-Object -First 1
|
||||||
if ($found) {
|
if ($found) {
|
||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
} elseif (Test-Path $V8Path -PathType Container) {
|
}
|
||||||
|
if (Test-Path $V8Path -PathType Container) {
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Validate connection (skip if DryRun) ---
|
# --- Detect engine + validate connection (skip if DryRun) ---
|
||||||
|
$engine = "1cv8"
|
||||||
if (-not $DryRun) {
|
if (-not $DryRun) {
|
||||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
function Invoke-IbcmdProcess {
|
||||||
|
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
||||||
|
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
||||||
|
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
||||||
|
param([string]$Exe, [string[]]$IbArgs)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
try {
|
||||||
|
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
} catch {}
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
$out = $p.StandardOutput.ReadToEnd()
|
||||||
|
$err = $p.StandardError.ReadToEnd()
|
||||||
|
$p.WaitForExit()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
if (-not $InfoBasePath) {
|
||||||
|
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -167,6 +227,15 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- Get changed files from Git ---
|
# --- Get changed files from Git ---
|
||||||
|
# Все git-вызовы для сбора путей идут через один хелпер с -c core.quotePath=false,
|
||||||
|
# иначе кириллические пути возвращаются в octal-виде и не распознаются (зеркало run_git в .py).
|
||||||
|
function Invoke-GitLines {
|
||||||
|
param([string[]]$GitArgs)
|
||||||
|
$out = git -c core.quotePath=false @GitArgs 2>&1
|
||||||
|
if ($LASTEXITCODE -eq 0) { return $out }
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
$changedFiles = @()
|
$changedFiles = @()
|
||||||
$ConfigDir = (Resolve-Path $ConfigDir).Path.TrimEnd('\')
|
$ConfigDir = (Resolve-Path $ConfigDir).Path.TrimEnd('\')
|
||||||
$configDirNormalized = $ConfigDir.Replace('\', '/')
|
$configDirNormalized = $ConfigDir.Replace('\', '/')
|
||||||
@@ -176,29 +245,22 @@ try {
|
|||||||
switch ($Source) {
|
switch ($Source) {
|
||||||
"Staged" {
|
"Staged" {
|
||||||
Write-Host "Getting staged changes..."
|
Write-Host "Getting staged changes..."
|
||||||
$raw = git diff --cached --name-only --relative 2>&1
|
$changedFiles += Invoke-GitLines -GitArgs @('diff', '--cached', '--name-only', '--relative')
|
||||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
|
||||||
}
|
}
|
||||||
"Unstaged" {
|
"Unstaged" {
|
||||||
Write-Host "Getting unstaged changes..."
|
Write-Host "Getting unstaged changes..."
|
||||||
$raw = git diff --name-only --relative 2>&1
|
$changedFiles += Invoke-GitLines -GitArgs @('diff', '--name-only', '--relative')
|
||||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
$changedFiles += Invoke-GitLines -GitArgs @('ls-files', '--others', '--exclude-standard')
|
||||||
$raw = git ls-files --others --exclude-standard 2>&1
|
|
||||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
|
||||||
}
|
}
|
||||||
"Commit" {
|
"Commit" {
|
||||||
Write-Host "Getting changes from $CommitRange..."
|
Write-Host "Getting changes from $CommitRange..."
|
||||||
$raw = git diff --name-only --relative $CommitRange 2>&1
|
$changedFiles += Invoke-GitLines -GitArgs @('diff', '--name-only', '--relative', $CommitRange)
|
||||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
|
||||||
}
|
}
|
||||||
"All" {
|
"All" {
|
||||||
Write-Host "Getting all uncommitted changes..."
|
Write-Host "Getting all uncommitted changes..."
|
||||||
$raw = git diff --cached --name-only --relative 2>&1
|
$changedFiles += Invoke-GitLines -GitArgs @('diff', '--cached', '--name-only', '--relative')
|
||||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
$changedFiles += Invoke-GitLines -GitArgs @('diff', '--name-only', '--relative')
|
||||||
$raw = git diff --name-only --relative 2>&1
|
$changedFiles += Invoke-GitLines -GitArgs @('ls-files', '--others', '--exclude-standard')
|
||||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
|
||||||
$raw = git ls-files --others --exclude-standard 2>&1
|
|
||||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -216,13 +278,16 @@ Write-Host "Git changes detected: $($changedFiles.Count) files"
|
|||||||
|
|
||||||
# --- Filter and map to config files ---
|
# --- Filter and map to config files ---
|
||||||
$configFiles = @()
|
$configFiles = @()
|
||||||
|
$supportSkipped = @()
|
||||||
|
|
||||||
foreach ($file in $changedFiles) {
|
foreach ($file in $changedFiles) {
|
||||||
$file = $file.Trim().Replace('\', '/')
|
$file = $file.Trim().Replace('\', '/')
|
||||||
if ([string]::IsNullOrWhiteSpace($file)) { continue }
|
if ([string]::IsNullOrWhiteSpace($file)) { continue }
|
||||||
|
|
||||||
# Skip service files
|
# Skip service files (not partially loadable). Support-state files are tracked
|
||||||
if ($file -eq "ConfigDumpInfo.xml") { continue }
|
# to warn the user: support changes apply only via a full load.
|
||||||
|
if ($file -match 'ParentConfigurations\.bin$') { $supportSkipped += $file; continue }
|
||||||
|
if ($file -eq "ConfigDumpInfo.xml" -or $file -match '(^|/)ConfigDumpInfo\.xml$') { continue }
|
||||||
|
|
||||||
$fullPath = Join-Path $ConfigDir $file
|
$fullPath = Join-Path $ConfigDir $file
|
||||||
|
|
||||||
@@ -265,6 +330,12 @@ foreach ($file in $changedFiles) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($supportSkipped.Count -gt 0) {
|
||||||
|
Write-Host "[ВНИМАНИЕ] Состояние поддержки изменено в коммите, но частично не загружается (исключено):" -ForegroundColor Yellow
|
||||||
|
foreach ($sf in $supportSkipped) { Write-Host " - $sf" -ForegroundColor Yellow }
|
||||||
|
Write-Host " Смена состояния поддержки применяется только полной загрузкой (db-load-xml -Mode Full)." -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
if ($configFiles.Count -eq 0) {
|
if ($configFiles.Count -eq 0) {
|
||||||
Write-Host "No configuration files found in changes"
|
Write-Host "No configuration files found in changes"
|
||||||
exit 0
|
exit 0
|
||||||
@@ -285,6 +356,53 @@ $tempDir = Join-Path $env:TEMP "db_load_git_$(Get-Random)"
|
|||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
# --- ibcmd branch (file infobase only; import specific files) ---
|
||||||
|
if ($Format -eq "Plain") {
|
||||||
|
Write-Host "Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($AllExtensions) {
|
||||||
|
Write-Host "Error: ibcmd config import does not support -AllExtensions (use -Extension or 1cv8)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
$arguments = @("infobase", "config", "import", "files") + $configFiles
|
||||||
|
$arguments += "--base-dir=$ConfigDir", "--db-path=$InfoBasePath"
|
||||||
|
if ($Extension) { $arguments += "--extension=$Extension" }
|
||||||
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
|
$arguments += "--data=$tempDir"
|
||||||
|
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
if ($exitCode -ne 0) {
|
||||||
|
Write-Host "Error loading changes (code: $exitCode)" -ForegroundColor Red
|
||||||
|
if ($output) { Write-Host ($output | Out-String) }
|
||||||
|
exit $exitCode
|
||||||
|
}
|
||||||
|
Write-Host "Changes loaded successfully ($($configFiles.Count) files)" -ForegroundColor Green
|
||||||
|
if ($output) { Write-Host ($output | Out-String) }
|
||||||
|
if ($UpdateDB) {
|
||||||
|
$applyArgs = @("infobase", "config", "apply", "--db-path=$InfoBasePath", "--force")
|
||||||
|
if ($UserName) { $applyArgs += "--user=$UserName" }
|
||||||
|
if ($Password) { $applyArgs += "--password=$Password" }
|
||||||
|
$applyArgs += "--data=$tempDir"
|
||||||
|
Write-Host "Running: ibcmd $($applyArgs -join ' ')"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $applyArgs
|
||||||
|
$applyOut = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
if ($exitCode -eq 0) {
|
||||||
|
Write-Host "Database configuration updated successfully" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "Error updating database configuration (code: $exitCode)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
if ($applyOut) { Write-Host ($applyOut | Out-String) }
|
||||||
|
}
|
||||||
|
exit $exitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1cv8 branch ---
|
||||||
# --- Write list file (UTF-8 with BOM) ---
|
# --- Write list file (UTF-8 with BOM) ---
|
||||||
$listFile = Join-Path $tempDir "load_list.txt"
|
$listFile = Join-Path $tempDir "load_list.txt"
|
||||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||||
+152
-17
@@ -1,9 +1,11 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-load-git v1.3 — Load Git changes into 1C database
|
# db-load-git v1.11 — Load Git changes into 1C database
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import atexit
|
||||||
import glob
|
import glob
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
@@ -13,26 +15,90 @@ import sys
|
|||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
|
def _find_project_v8path():
|
||||||
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
|
d = os.getcwd()
|
||||||
|
while True:
|
||||||
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pf):
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
v = data.get("v8path")
|
||||||
|
if v:
|
||||||
|
return v
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return None
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
|
def _version_key(p):
|
||||||
|
"""Numeric sort key from version dir name."""
|
||||||
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def resolve_v8path(v8path):
|
||||||
"""Resolve path to 1cv8.exe."""
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
if not v8path:
|
if not v8path:
|
||||||
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
v8path = _find_project_v8path()
|
||||||
if candidates:
|
if not v8path:
|
||||||
candidates.sort()
|
if os.name == "nt":
|
||||||
return candidates[-1]
|
candidates = (
|
||||||
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
|
if candidates:
|
||||||
|
v8path = max(candidates, key=_version_key)
|
||||||
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
|
else:
|
||||||
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
elif os.path.isdir(v8path):
|
if os.path.isdir(v8path):
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
def get_object_xml_from_subfile(relative_path):
|
def get_object_xml_from_subfile(relative_path):
|
||||||
"""Map sub-file path (BSL, HTML, etc.) to object XML path."""
|
"""Map sub-file path (BSL, HTML, etc.) to object XML path."""
|
||||||
parts = re.split(r"[\\/]", relative_path)
|
parts = re.split(r"[\\/]", relative_path)
|
||||||
@@ -44,7 +110,7 @@ def get_object_xml_from_subfile(relative_path):
|
|||||||
def run_git(config_dir, git_args):
|
def run_git(config_dir, git_args):
|
||||||
"""Run a git command in config_dir and return output lines on success."""
|
"""Run a git command in config_dir and return output lines on success."""
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["git"] + git_args,
|
["git", "-c", "core.quotePath=false"] + git_args,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
@@ -93,9 +159,15 @@ def main():
|
|||||||
if not args.DryRun:
|
if not args.DryRun:
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
|
||||||
# --- Validate connection (skip if DryRun) ---
|
# --- Detect engine + validate connection (skip if DryRun) ---
|
||||||
|
engine = "1cv8"
|
||||||
if not args.DryRun:
|
if not args.DryRun:
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
if engine == "ibcmd":
|
||||||
|
if not args.InfoBasePath:
|
||||||
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@@ -146,14 +218,19 @@ def main():
|
|||||||
|
|
||||||
# --- Filter and map to config files ---
|
# --- Filter and map to config files ---
|
||||||
config_files = []
|
config_files = []
|
||||||
|
support_skipped = []
|
||||||
|
|
||||||
for file in changed_files:
|
for file in changed_files:
|
||||||
file = file.strip().replace("\\", "/")
|
file = file.strip().replace("\\", "/")
|
||||||
if not file:
|
if not file:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Skip service files
|
# Skip service files (not partially loadable). Support-state files are
|
||||||
if file == "ConfigDumpInfo.xml":
|
# tracked to warn: support changes apply only via a full load.
|
||||||
|
if file.endswith("ParentConfigurations.bin"):
|
||||||
|
support_skipped.append(file)
|
||||||
|
continue
|
||||||
|
if file == "ConfigDumpInfo.xml" or file.endswith("/ConfigDumpInfo.xml"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
full_path = os.path.join(args.ConfigDir, file)
|
full_path = os.path.join(args.ConfigDir, file)
|
||||||
@@ -186,6 +263,12 @@ def main():
|
|||||||
if rel_path not in config_files:
|
if rel_path not in config_files:
|
||||||
config_files.append(rel_path)
|
config_files.append(rel_path)
|
||||||
|
|
||||||
|
if support_skipped:
|
||||||
|
print("[ВНИМАНИЕ] Состояние поддержки изменено в коммите, но частично не загружается (исключено):", file=sys.stderr)
|
||||||
|
for sf in support_skipped:
|
||||||
|
print(f" - {sf}", file=sys.stderr)
|
||||||
|
print(" Смена состояния поддержки применяется только полной загрузкой (db-load-xml -Mode Full).", file=sys.stderr)
|
||||||
|
|
||||||
if len(config_files) == 0:
|
if len(config_files) == 0:
|
||||||
print("No configuration files found in changes")
|
print("No configuration files found in changes")
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
@@ -205,6 +288,58 @@ def main():
|
|||||||
os.makedirs(temp_dir, exist_ok=True)
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if engine == "ibcmd":
|
||||||
|
# --- ibcmd branch (file infobase only; import specific files) ---
|
||||||
|
if args.Format == "Plain":
|
||||||
|
print("Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if args.AllExtensions:
|
||||||
|
print("Error: ibcmd config import does not support -AllExtensions (use -Extension or 1cv8)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
arguments = ["infobase", "config", "import", "files"] + config_files
|
||||||
|
arguments += [f"--base-dir={args.ConfigDir}", f"--db-path={args.InfoBasePath}"]
|
||||||
|
if args.Extension:
|
||||||
|
arguments.append(f"--extension={args.Extension}")
|
||||||
|
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
||||||
|
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
||||||
|
if args.UserName:
|
||||||
|
arguments.append(f"--user={args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
arguments.append(f"--password={args.Password}")
|
||||||
|
arguments.append(f"--data={ib_data}")
|
||||||
|
print(f"Running: ibcmd {' '.join(arguments)}")
|
||||||
|
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(f"Error loading changes (code: {result.returncode})", file=sys.stderr)
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
sys.exit(result.returncode)
|
||||||
|
print(f"Changes loaded successfully ({len(config_files)} files)")
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
exit_code = 0
|
||||||
|
if args.UpdateDB:
|
||||||
|
apply_args = ["infobase", "config", "apply", f"--db-path={args.InfoBasePath}", "--force"]
|
||||||
|
if args.UserName:
|
||||||
|
apply_args.append(f"--user={args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
apply_args.append(f"--password={args.Password}")
|
||||||
|
apply_args.append(f"--data={ib_data}")
|
||||||
|
print(f"Running: ibcmd {' '.join(apply_args)}")
|
||||||
|
ar = run_ibcmd([v8path] + apply_args, bool(args.UserName))
|
||||||
|
exit_code = ar.returncode
|
||||||
|
if exit_code == 0:
|
||||||
|
print("Database configuration updated successfully")
|
||||||
|
else:
|
||||||
|
print(f"Error updating database configuration (code: {exit_code})", file=sys.stderr)
|
||||||
|
if ar.stdout:
|
||||||
|
print(ar.stdout)
|
||||||
|
if ar.stderr:
|
||||||
|
print(ar.stderr, file=sys.stderr)
|
||||||
|
sys.exit(exit_code)
|
||||||
|
|
||||||
# --- Write list file (UTF-8 with BOM) ---
|
# --- Write list file (UTF-8 with BOM) ---
|
||||||
list_file = os.path.join(temp_dir, "load_list.txt")
|
list_file = os.path.join(temp_dir, "load_list.txt")
|
||||||
with open(list_file, "w", encoding="utf-8-sig") as f:
|
with open(list_file, "w", encoding="utf-8-sig") as f:
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: db-load-xml
|
name: db-load-xml
|
||||||
description: Загрузка конфигурации 1С из XML-файлов. Используй когда пользователь просит загрузить конфигурацию из файлов, XML, исходников, LoadConfigFromFiles
|
description: Загрузка конфигурации 1С из XML-файлов. Используй когда нужно загрузить конфигурацию из файлов, XML, исходников, LoadConfigFromFiles
|
||||||
argument-hint: <configDir> [database]
|
argument-hint: <configDir> [database]
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash
|
- Bash
|
||||||
@@ -30,7 +30,7 @@ allowed-tools:
|
|||||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||||
4. Если ветка не совпала — используй `default`
|
4. Если ветка не совпала — используй `default`
|
||||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||||
Если файла нет — предложи `/db-list add`.
|
Если файла нет — предложи `/db-list add`.
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
Если в записи базы указан `configSrc` — используй как каталог загрузки по умолчанию.
|
Если в записи базы указан `configSrc` — используй как каталог загрузки по умолчанию.
|
||||||
@@ -38,14 +38,14 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-load-xml/scripts/db-load-xml.ps1 <параметры>
|
python ".opencode/skills/db-load-xml/scripts/db-load-xml.py" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
| Параметр | Обязательный | Описание |
|
||||||
|----------|:------------:|----------|
|
|----------|:------------:|----------|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
@@ -80,30 +80,22 @@ Documents/Заказ.xml
|
|||||||
Documents/Заказ/Forms/ФормаДокумента.xml
|
Documents/Заказ/Forms/ФормаДокумента.xml
|
||||||
```
|
```
|
||||||
|
|
||||||
## Коды возврата
|
|
||||||
|
|
||||||
| Код | Описание |
|
|
||||||
|-----|----------|
|
|
||||||
| 0 | Успешно |
|
|
||||||
| 1 | Ошибка (см. лог) |
|
|
||||||
|
|
||||||
## После выполнения
|
## После выполнения
|
||||||
|
|
||||||
1. Прочитай лог и покажи результат
|
Если `-UpdateDB` не был указан — **предложи выполнить `/db-update`** для применения изменений к БД
|
||||||
2. Если `-UpdateDB` не был указан — **предложи выполнить `/db-update`** для применения изменений к БД
|
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Полная загрузка
|
# Полная загрузка
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-load-xml/scripts/db-load-xml.ps1 -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full
|
python ".opencode/skills/db-load-xml/scripts/db-load-xml.py" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full
|
||||||
|
|
||||||
# Частичная загрузка конкретных файлов
|
# Частичная загрузка конкретных файлов
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-load-xml/scripts/db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl"
|
python ".opencode/skills/db-load-xml/scripts/db-load-xml.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl"
|
||||||
|
|
||||||
# Загрузка расширения
|
# Загрузка расширения
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-load-xml/scripts/db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
|
python ".opencode/skills/db-load-xml/scripts/db-load-xml.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
|
||||||
|
|
||||||
# Загрузка + обновление БД в одном запуске
|
# Загрузка + обновление БД в одном запуске
|
||||||
powershell.exe -NoProfile -File .claude/skills/db-load-xml/scripts/db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full -UpdateDB
|
python ".opencode/skills/db-load-xml/scripts/db-load-xml.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full -UpdateDB
|
||||||
```
|
```
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user