Auto-build: copilot (powershell) from 5472e03

This commit is contained in:
github-actions[bot]
2026-07-26 14:29:03 +00:00
commit 6be08f13a4
321 changed files with 157107 additions and 0 deletions
@@ -0,0 +1,562 @@
# xdto-edit v1.0 — Point edits of a 1C XDTO package
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory=$true)]
[Alias('Path')]
[string]$PackagePath,
[Parameter(Mandatory=$true)]
[ValidateSet("add-property", "replace-property", "remove-property",
"add-type", "remove-type", "add-enum", "add-import",
"rename", "set-synonym", "set-comment", "set-namespace")]
[string]$Operation,
[string]$Target,
[string]$Value,
[switch]$NoValidate
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$XDTO_NS = "http://v8.1c.ru/8.1/xdto"
$XS_NS = "http://www.w3.org/2001/XMLSchema"
$MD_NS = "http://v8.1c.ru/8.3/MDClasses"
$V8_NS = "http://v8.1c.ru/8.1/data/core"
# --- 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 Test-ExternalObjectRoot([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $false }
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) { return @('ExternalDataProcessor','ExternalReport') -contains $el.get_LocalName() }
} catch {}
return $false
}
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) {
$mode = "deny"
try {
$pj = Find-V8Project $cfgDir
if ($pj) {
$cfg = Get-Content -Path $pj -Raw -Encoding UTF8 | ConvertFrom-Json
if ($cfg.PSObject.Properties.Name -contains 'editingAllowedCheck' -and $cfg.editingAllowedCheck) {
$mode = [string]$cfg.editingAllowedCheck
}
}
} catch {}
return $mode
}
function Assert-EditAllowed([string]$targetPath) {
try {
$d = $targetPath
for ($i = 0; $i -lt 20 -and $d; $i++) {
foreach ($x in @(Get-ChildItem -Path $d -Filter "*.xml" -File -ErrorAction SilentlyContinue)) {
if (Test-ExternalObjectRoot $x.FullName) { return }
}
$cfgXml = Join-Path $d "Configuration.xml"
$supportBin = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
if (Test-Path $cfgXml) {
if (Test-Path $supportBin) {
$mode = Get-EditMode $d
if ($mode -eq "off") { return }
$msg = "Конфигурация находится на поддержке (Ext/ParentConfigurations.bin). Правка может быть запрещена."
if ($mode -eq "warn") { Write-Warning $msg; return }
throw "$msg Снимите с поддержки (/support-edit) или задайте editingAllowedCheck в .v8-project.json."
}
return
}
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
} catch [System.Management.Automation.RuntimeException] {
throw
} catch {}
}
# --- Resolve package ------------------------------------------------------------
if (-not [System.IO.Path]::IsPathRooted($PackagePath)) {
$PackagePath = Join-Path (Get-Location).Path $PackagePath
}
$pkgDir = $null
if (Test-Path $PackagePath -PathType Container) {
if (Test-Path (Join-Path (Join-Path $PackagePath "Ext") "Package.bin")) { $pkgDir = $PackagePath }
} elseif ((Test-Path $PackagePath -PathType Leaf) -and ([System.IO.Path]::GetFileName($PackagePath) -eq "Package.bin")) {
$pkgDir = Split-Path (Split-Path $PackagePath -Parent) -Parent
} elseif ($PackagePath.EndsWith(".xml")) {
$stem = [System.IO.Path]::Combine([System.IO.Path]::GetDirectoryName($PackagePath),
[System.IO.Path]::GetFileNameWithoutExtension($PackagePath))
if (Test-Path (Join-Path (Join-Path $stem "Ext") "Package.bin")) { $pkgDir = $stem }
}
if (-not $pkgDir) { throw "Не найден пакет XDTO по пути: $PackagePath" }
$pkgName = [System.IO.Path]::GetFileName($pkgDir)
$xdtoRoot = Split-Path $pkgDir -Parent
$configRoot = Split-Path $xdtoRoot -Parent
$binFile = Join-Path (Join-Path $pkgDir "Ext") "Package.bin"
$mdFile = Join-Path $xdtoRoot "$pkgName.xml"
$configXml = Join-Path $configRoot "Configuration.xml"
# -Value "@путь" — содержимое берётся из файла. Передавать XSD-фрагмент инлайном
# через powershell.exe -File ненадёжно: вложенные кавычки схлопываются на границе
# процессов, и вместо понятной ошибки получается сырой сбой разбора XML.
if ($Value -and $Value.StartsWith("@")) {
$valueFile = $Value.Substring(1)
if (-not [System.IO.Path]::IsPathRooted($valueFile)) {
$valueFile = Join-Path (Get-Location).Path $valueFile
}
if (-not (Test-Path $valueFile -PathType Leaf)) { throw "Файл значения не найден: $valueFile" }
$Value = [System.IO.File]::ReadAllText($valueFile).Trim()
}
Assert-EditAllowed $pkgDir
$encBom = New-Object System.Text.UTF8Encoding($true)
# --- Sibling skills -------------------------------------------------------------
# Правка идёт через уже проверенный round-trip: пакет выгружается в XSD, операция
# применяется к схеме, пакет собирается обратно. Второго эмиттера не заводим —
# байт-точность для всего нетронутого достаётся от компилятора.
$decompileScript = Join-Path (Join-Path $PSScriptRoot "..\..\xdto-decompile") "scripts\xdto-decompile.ps1"
$compileScript = Join-Path (Join-Path $PSScriptRoot "..\..\xdto-compile") "scripts\xdto-compile.ps1"
$validateScript = Join-Path (Join-Path $PSScriptRoot "..\..\xdto-validate") "scripts\xdto-validate.ps1"
# Исключение из автономности навыков, сделанное осознанно: конвертер XSD ↔ модель
# нельзя скопировать буквально (xdto-compile — скрипт со сквозным потоком, не библиотека),
# а вторая его реализация разошлась бы с первой. Обещание «правка не меняет ни байта
# в нетронутом» держится именно на том, что код тот же самый.
# Проверяем комплектность заранее, чтобы не падать на середине правки.
function Assert-SiblingsPresent([string]$operation) {
$needed = @{}
if (@("rename", "set-synonym", "set-comment") -notcontains $operation) {
$needed["xdto-decompile"] = $decompileScript
$needed["xdto-compile"] = $compileScript
}
$missing = @()
foreach ($k in $needed.Keys) { if (-not (Test-Path $needed[$k])) { $missing += $k } }
if ($missing.Count -gt 0) {
$skillsRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
throw ("Навык неработоспособен: рядом нет " + ($missing -join ", ") + ".`n" +
"Операция `"$operation`" выполняется через " +
$(if ($missing.Count -gt 1) { "них" } else { "него" }) + ".`n" +
"Ожидаются в каталоге навыков: $skillsRoot")
}
}
function Invoke-Sibling([string]$script, [string[]]$argList, [string]$what) {
if (-not (Test-Path $script)) { throw "Не найден навык $what по пути: $script" }
$out = & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $script @argList 2>&1
if ($LASTEXITCODE -ne 0) { throw "$what завершился с ошибкой:`n$($out -join "`n")" }
return $out
}
# --- Metadata object edits ------------------------------------------------------
function Edit-Metadata([string]$field, [string]$newValue) {
$doc = New-Object System.Xml.XmlDocument
$doc.PreserveWhitespace = $true
$doc.Load($mdFile)
$nsm = New-Object System.Xml.XmlNamespaceManager($doc.NameTable)
$nsm.AddNamespace("md", $MD_NS)
$nsm.AddNamespace("v8", $V8_NS)
$props = $doc.SelectSingleNode("//md:XDTOPackage/md:Properties", $nsm)
if (-not $props) { throw "В объекте метаданных не найден блок <Properties>" }
switch ($field) {
"Name" {
$n = $props.SelectSingleNode("md:Name", $nsm)
if (-not $n) { throw "В объекте метаданных нет <Name>" }
$n.InnerText = $newValue
}
"Comment" {
$c = $props.SelectSingleNode("md:Comment", $nsm)
if (-not $c) {
$c = $doc.CreateElement("Comment", $MD_NS)
$props.AppendChild($c) | Out-Null
}
$c.InnerText = $newValue
}
"Namespace" {
$ns = $props.SelectSingleNode("md:Namespace", $nsm)
if (-not $ns) { throw "В объекте метаданных нет <Namespace>" }
$ns.InnerText = $newValue
}
"Synonym" {
$syn = $props.SelectSingleNode("md:Synonym", $nsm)
if (-not $syn) {
$syn = $doc.CreateElement("Synonym", $MD_NS)
$props.AppendChild($syn) | Out-Null
}
$item = $syn.SelectSingleNode("v8:item[v8:lang='ru']", $nsm)
if (-not $item) {
$item = $doc.CreateElement("v8", "item", $V8_NS)
$lang = $doc.CreateElement("v8", "lang", $V8_NS); $lang.InnerText = "ru"
$cont = $doc.CreateElement("v8", "content", $V8_NS)
$item.AppendChild($lang) | Out-Null
$item.AppendChild($cont) | Out-Null
$syn.AppendChild($item) | Out-Null
}
$item.SelectSingleNode("v8:content", $nsm).InnerText = $newValue
}
}
$settings = New-Object System.Xml.XmlWriterSettings
$settings.Encoding = $encBom
$settings.Indent = $false
$stream = New-Object System.IO.FileStream($mdFile, [System.IO.FileMode]::Create)
$writer = [System.Xml.XmlWriter]::Create($stream, $settings)
$doc.Save($writer)
$writer.Close(); $stream.Close()
}
function Rename-Package([string]$newName) {
if ($newName -notmatch '^[\wЀ-ӿ]+$' -or $newName -match '^\d') {
throw "`"$newName`" не является допустимым идентификатором 1С"
}
$newMd = Join-Path $xdtoRoot "$newName.xml"
$newDir = Join-Path $xdtoRoot $newName
if ((Test-Path $newMd) -or (Test-Path $newDir)) { throw "Имя `"$newName`" уже занято другим пакетом" }
Edit-Metadata "Name" $newName
Move-Item $mdFile $newMd
Move-Item $pkgDir $newDir
if (Test-Path $configXml) {
$cfg = New-Object System.Xml.XmlDocument
$cfg.PreserveWhitespace = $true
$cfg.Load($configXml)
$nsm = New-Object System.Xml.XmlNamespaceManager($cfg.NameTable)
$nsm.AddNamespace("md", $MD_NS)
$found = $false
foreach ($e in $cfg.SelectNodes("//md:Configuration/md:ChildObjects/md:XDTOPackage", $nsm)) {
if ($e.InnerText -eq $pkgName) { $e.InnerText = $newName; $found = $true; break }
}
if ($found) {
$s = New-Object System.Xml.XmlWriterSettings
$s.Encoding = $encBom; $s.Indent = $false
$st = New-Object System.IO.FileStream($configXml, [System.IO.FileMode]::Create)
$w = [System.Xml.XmlWriter]::Create($st, $s)
$cfg.Save($w); $w.Close(); $st.Close()
Write-Host " Configuration.xml: <XDTOPackage> переименован в $newName"
} else {
Write-Warning "В Configuration.xml не найдена запись <XDTOPackage>$pkgName</XDTOPackage> — зарегистрируйте пакет вручную"
}
}
Write-Host "✓ Пакет переименован: $pkgName$newName"
Write-Host " Перемещены: $newName.xml, $newName/"
}
# --- Model edits through the XSD round-trip -------------------------------------
$XSD_DECL = @{ "add-property" = "element"; "replace-property" = "element"; "remove-property" = "element" }
function Get-SchemaChildren([System.Xml.XmlElement]$el, [string]$local) {
$res = New-Object System.Collections.ArrayList
foreach ($c in $el.ChildNodes) {
if ($c.NodeType -eq [System.Xml.XmlNodeType]::Element -and $c.NamespaceURI -eq $XS_NS -and $c.get_LocalName() -eq $local) { [void]$res.Add($c) }
}
return ,$res
}
function Get-SchemaFirst([System.Xml.XmlElement]$el, [string]$local) {
$r = Get-SchemaChildren $el $local
if ($r.Count -gt 0) { return $r[0] }
return $null
}
function Find-TypeElement($schema, [string]$typeName) {
foreach ($kind in @("complexType", "simpleType")) {
foreach ($t in (Get-SchemaChildren $schema $kind)) {
if ($t.GetAttribute("name") -eq $typeName) { return $t }
}
}
throw "В пакете нет типа `"$typeName`""
}
# Тело типа: внутрь xs:complexContent/xs:extension, если тип наследуется
function Get-TypeBody([System.Xml.XmlElement]$ct) {
$content = Get-SchemaFirst $ct "complexContent"
if ($content) {
$ext = Get-SchemaFirst $content "extension"
if ($ext) { return $ext }
}
return $ct
}
function Find-Declaration([System.Xml.XmlElement]$body, [string]$propName) {
foreach ($node in $body.SelectNodes(".//*")) {
if ($node.NamespaceURI -ne $XS_NS) { continue }
if (@("element", "attribute") -notcontains $node.get_LocalName()) { continue }
if ($node.GetAttribute("name") -eq $propName) { return $node }
}
return $null
}
# Путь Тип.Свойство[.Вложенное...] — точка безопасна: имена в модели XDTO
# являются идентификаторами 1С и точку содержать не могут
function Resolve-Path($schema, [string]$path) {
$segments = $path.Split(".")
$typeEl = Find-TypeElement $schema $segments[0]
if ($segments.Count -eq 1) { return [pscustomobject]@{ Type = $typeEl; Decl = $null } }
$body = Get-TypeBody $typeEl
$decl = $null
for ($i = 1; $i -lt $segments.Count; $i++) {
$decl = Find-Declaration $body $segments[$i]
if (-not $decl) { throw "По пути `"$path`" не найдено свойство `"$($segments[$i])`"" }
if ($i -lt $segments.Count - 1) {
$inner = Get-SchemaFirst $decl "complexType"
if (-not $inner) { throw "Свойство `"$($segments[$i])`" не содержит вложенного типа — путь дальше не идёт" }
$body = Get-TypeBody $inner
}
}
return [pscustomobject]@{ Type = $typeEl; Decl = $decl }
}
function Import-Fragment($schema, [string]$xml) {
$tmp = New-Object System.Xml.XmlDocument
$nsAttrs = " xmlns:xs=`"$XS_NS`" xmlns:xdto=`"$XDTO_NS`""
$tns = $schema.GetAttribute("targetNamespace")
if ($tns) { $nsAttrs += " xmlns:tns=`"$tns`"" }
foreach ($a in $schema.Attributes) {
if ($a.Prefix -eq "xmlns" -and $a.get_LocalName() -notin @("xs", "xdto", "tns")) {
$nsAttrs += " xmlns:$($a.get_LocalName())=`"$($a.Value)`""
}
}
try { $tmp.LoadXml("<wrap$nsAttrs>$xml</wrap>") }
catch {
throw ("Не удалось разобрать -Value как фрагмент XML-схемы: " + $_.Exception.InnerException.Message + "`n" +
"Получено: " + $xml + "`n" +
"Если фрагмент передан инлайном, кавычки могли схлопнуться на границе процессов — " +
"положите его в файл и укажите -Value `"@путь`".")
}
$res = New-Object System.Collections.ArrayList
foreach ($c in $tmp.DocumentElement.ChildNodes) {
if ($c.NodeType -eq [System.Xml.XmlNodeType]::Element) {
[void]$res.Add($schema.OwnerDocument.ImportNode($c, $true))
}
}
if ($res.Count -eq 0) { throw "Во фрагменте нет ни одного элемента: $xml" }
return ,$res
}
function Apply-ModelOperation($schema) {
switch ($Operation) {
"add-property" {
if (-not $Target) { throw "add-property требует -Target <Тип>" }
$loc = Resolve-Path $schema $Target
$body = Get-TypeBody $(if ($loc.Decl) { Get-SchemaFirst $loc.Decl "complexType" } else { $loc.Type })
foreach ($frag in (Import-Fragment $schema $Value)) {
$kind = $frag.get_LocalName()
if ($kind -eq "attribute") {
$body.AppendChild($frag) | Out-Null
} elseif ($kind -eq "element") {
$particle = Get-SchemaFirst $body "sequence"
if (-not $particle) { $particle = Get-SchemaFirst $body "choice" }
if (-not $particle) { $particle = Get-SchemaFirst $body "all" }
if (-not $particle) {
$particle = $schema.OwnerDocument.CreateElement("xs", "sequence", $XS_NS)
$firstAttr = Get-SchemaFirst $body "attribute"
if ($firstAttr) { $body.InsertBefore($particle, $firstAttr) | Out-Null }
else { $body.AppendChild($particle) | Out-Null }
}
$particle.AppendChild($frag) | Out-Null
} else {
throw "add-property ожидает <xs:element> или <xs:attribute>, получен <xs:$kind>"
}
Write-Host " + $($frag.GetAttribute('name')) в тип $Target"
}
}
"replace-property" {
if (-not $Target) { throw "replace-property требует -Target `"Тип.Свойство`"" }
$loc = Resolve-Path $schema $Target
if (-not $loc.Decl) { throw "replace-property требует путь вида `"Тип.Свойство`"" }
$frags = Import-Fragment $schema $Value
if ($frags.Count -ne 1) { throw "replace-property ожидает ровно одно объявление" }
$loc.Decl.ParentNode.ReplaceChild($frags[0], $loc.Decl) | Out-Null
Write-Host " ~ $Target заменено"
}
"remove-property" {
if (-not $Target) { throw "remove-property требует путь `"Тип.Свойство`"" }
foreach ($one in ($Target -split "\s*;;\s*")) {
if (-not $one) { continue }
$loc = Resolve-Path $schema $one
if (-not $loc.Decl) { throw "remove-property требует путь вида `"Тип.Свойство`", получено `"$one`"" }
$loc.Decl.ParentNode.RemoveChild($loc.Decl) | Out-Null
Write-Host " $one удалено"
}
}
"add-type" {
foreach ($frag in (Import-Fragment $schema $Value)) {
if (@("complexType", "simpleType") -notcontains $frag.get_LocalName()) {
throw "add-type ожидает <xs:complexType> или <xs:simpleType>, получен <xs:$($frag.get_LocalName())>"
}
$schema.AppendChild($frag) | Out-Null
Write-Host " + тип $($frag.GetAttribute('name'))"
}
}
"remove-type" {
if (-not $Target) { throw "remove-type требует -Target <Тип>" }
foreach ($one in ($Target -split "\s*;;\s*")) {
if (-not $one) { continue }
$t = Find-TypeElement $schema $one
$t.ParentNode.RemoveChild($t) | Out-Null
Write-Host " тип $one удалён"
}
}
"add-enum" {
if (-not $Target) { throw "add-enum требует -Target <ТипЗначения>" }
$t = Find-TypeElement $schema $Target
$restriction = Get-SchemaFirst $t "restriction"
if (-not $restriction) { throw "Тип `"$Target`" не является ограничением простого типа" }
foreach ($lit in ($Value -split "\s*;;\s*")) {
if (-not $lit) { continue }
$e = $schema.OwnerDocument.CreateElement("xs", "enumeration", $XS_NS)
$e.SetAttribute("value", $lit)
$restriction.AppendChild($e) | Out-Null
Write-Host " + значение `"$lit`" в тип $Target"
}
}
"add-import" {
foreach ($uri in ($Value -split "\s*;;\s*")) {
if (-not $uri) { continue }
$exists = $false
foreach ($i in (Get-SchemaChildren $schema "import")) {
if ($i.GetAttribute("namespace") -eq $uri) { $exists = $true; break }
}
if ($exists) { Write-Host " = импорт $uri уже объявлен"; continue }
$imp = $schema.OwnerDocument.CreateElement("xs", "import", $XS_NS)
$imp.SetAttribute("namespace", $uri)
$firstOther = $null
foreach ($c in $schema.ChildNodes) {
if ($c.NodeType -eq [System.Xml.XmlNodeType]::Element -and $c.get_LocalName() -notin @("annotation", "import")) { $firstOther = $c; break }
}
if ($firstOther) { $schema.InsertBefore($imp, $firstOther) | Out-Null } else { $schema.AppendChild($imp) | Out-Null }
Write-Host " + импорт $uri"
}
}
"set-namespace" {
if (-not $Value) { throw "set-namespace требует -Value <URI>" }
$old = $schema.GetAttribute("targetNamespace")
# Установка того же значения не отбрасывается: пакет пересобирается вхолостую,
# и это заодно проба точности пути «выгрузить → собрать» на любом пакете
if ($old -eq $Value) { Write-Host " = namespace уже $Value, пакет пересобран без изменений" }
# Меняем и targetNamespace, и объявление префикса, который на него указывал:
# иначе ссылки на собственные типы станут ссылками в чужое пространство имён
$schema.SetAttribute("targetNamespace", $Value)
foreach ($a in @($schema.Attributes)) {
if ($a.Prefix -eq "xmlns" -and $a.Value -eq $old) {
$schema.SetAttribute("xmlns:$($a.get_LocalName())", $Value)
}
}
Write-Host " ~ namespace: $old$Value"
}
}
}
# --- Dispatch -------------------------------------------------------------------
$metaOps = @("rename", "set-synonym", "set-comment")
$touchesModel = ($metaOps -notcontains $Operation)
Assert-SiblingsPresent $Operation
Write-Host "Пакет: $pkgName"
if ($Operation -eq "rename") {
if (-not $Value) { throw "rename требует -Value <НовоеИмя>" }
Rename-Package $Value
$pkgName = $Value
$pkgDir = Join-Path $xdtoRoot $Value
} elseif ($Operation -eq "set-synonym") {
if (-not $Value) { throw "set-synonym требует -Value <текст>" }
Edit-Metadata "Synonym" $Value
Write-Host "✓ Синоним: $Value"
} elseif ($Operation -eq "set-comment") {
Edit-Metadata "Comment" $Value
Write-Host "✓ Комментарий обновлён"
} else {
$tmpDir = Join-Path ([System.IO.Path]::GetTempPath()) ("xdto-edit_" + [guid]::NewGuid().ToString("N").Substring(0, 8))
New-Item -ItemType Directory -Path $tmpDir -Force | Out-Null
try {
$xsdPath = Join-Path $tmpDir "schema.xsd"
Invoke-Sibling $decompileScript @("-PackagePath", $binFile, "-OutFile", $xsdPath) "xdto-decompile" | Out-Null
$doc = New-Object System.Xml.XmlDocument
$doc.PreserveWhitespace = $false
$doc.Load($xsdPath)
$schema = $doc.DocumentElement
$oldNamespace = $schema.GetAttribute("targetNamespace")
Apply-ModelOperation $schema
$doc.Save($xsdPath)
Invoke-Sibling $compileScript @("-XsdPath", $xsdPath, "-OutputDir", $configRoot, "-Name", $pkgName, "-Force") "xdto-compile" | Out-Null
if ($Operation -eq "set-namespace") {
Edit-Metadata "Namespace" $Value
# Зависящие пакеты не трогаем: при версионировании они обязаны продолжать
# смотреть на прежний namespace. Но молчать о них нельзя.
$dependents = @()
foreach ($d in (Get-ChildItem $xdtoRoot -Directory -ErrorAction SilentlyContinue)) {
if ($d.Name -eq $pkgName) { continue }
$ob = Join-Path (Join-Path $d.FullName "Ext") "Package.bin"
if (-not (Test-Path $ob)) { continue }
try {
$od = New-Object System.Xml.XmlDocument
$od.Load($ob)
foreach ($imp in $od.DocumentElement.ChildNodes) {
if ($imp.NodeType -eq [System.Xml.XmlNodeType]::Element -and $imp.get_LocalName() -eq "import" -and
$imp.GetAttribute("namespace") -eq $oldNamespace) { $dependents += $d.Name; break }
}
} catch {}
}
if ($dependents.Count -gt 0) {
Write-Host ""
Write-Warning ("Старый namespace импортируют пакеты ($($dependents.Count)): " + ($dependents -join ", ") +
". Они не изменены — при версионировании это верно; если нет, поправьте их импорты.")
}
}
Write-Host "✓ Пакет пересобран: XDTOPackages/$pkgName/Ext/Package.bin"
} finally {
Remove-Item $tmpDir -Recurse -Force -ErrorAction SilentlyContinue
}
}
# --- Validate -------------------------------------------------------------------
if (-not $NoValidate) {
if (Test-Path $validateScript) {
Write-Host ""
Write-Host "--- xdto-validate ---"
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $validateScript -PackagePath (Join-Path $xdtoRoot $pkgName)
} else {
Write-Host "[SKIP] xdto-validate не найден: $validateScript"
}
}
exit 0
@@ -0,0 +1,541 @@
# xdto-edit v1.0 — Point edits of a 1C XDTO package (Python port)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
import os
import re
import shutil
import subprocess
import sys
import tempfile
import uuid
from lxml import etree
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
XDTO_NS = "http://v8.1c.ru/8.1/xdto"
XS_NS = "http://www.w3.org/2001/XMLSchema"
MD_NS = "http://v8.1c.ru/8.3/MDClasses"
V8_NS = "http://v8.1c.ru/8.1/data/core"
OPS = ["add-property", "replace-property", "remove-property", "add-type", "remove-type",
"add-enum", "add-import", "rename", "set-synonym", "set-comment", "set-namespace"]
parser = argparse.ArgumentParser(allow_abbrev=False)
parser.add_argument("-PackagePath", "-Path", required=True)
parser.add_argument("-Operation", required=True, choices=OPS)
parser.add_argument("-Target", default="")
parser.add_argument("-Value", default="")
parser.add_argument("-NoValidate", action="store_true")
args = parser.parse_args()
def die(msg):
print(msg, file=sys.stderr)
sys.exit(1)
def local(el):
return etree.QName(el).localname
def _parse_xml(source, from_string=False):
"""Разбор с узким отступлением для не-URI пространств имён.
Платформа допускает в targetNamespace произвольную строку, .NET такое принимает,
а libxml2 отвергает. Откатываемся на восстанавливающий разбор ТОЛЬКО на этой ошибке.
"""
try:
return (etree.fromstring(source) if from_string else etree.parse(source))
except etree.XMLSyntaxError as e:
if "is not a valid URI" not in str(e):
raise
p = etree.XMLParser(recover=True)
return (etree.fromstring(source, p) if from_string else etree.parse(source, p))
# ── support guard ────────────────────────────────────────────
# См. docs/1c-support-state-spec.md.
def find_v8_project(start_dir):
d = os.path.abspath(start_dir)
for _ in range(20):
pj = os.path.join(d, ".v8-project.json")
if os.path.exists(pj):
return pj
parent = os.path.dirname(d)
if parent == d:
break
d = parent
return None
def get_edit_mode(cfg_dir):
try:
pj = find_v8_project(cfg_dir)
if pj:
with open(pj, encoding="utf-8-sig") as f:
return str(json.load(f).get("editingAllowedCheck") or "deny")
except Exception: # noqa: BLE001
pass
return "deny"
def is_external_object_root(xml_path):
try:
for el in _parse_xml(xml_path).getroot():
if isinstance(el.tag, str):
return local(el) in ("ExternalDataProcessor", "ExternalReport")
except Exception: # noqa: BLE001
pass
return False
def assert_edit_allowed(target_path):
d = os.path.abspath(target_path)
for _ in range(20):
try:
for f in os.listdir(d):
if f.endswith(".xml") and is_external_object_root(os.path.join(d, f)):
return
except OSError:
pass
if os.path.exists(os.path.join(d, "Configuration.xml")):
if os.path.exists(os.path.join(d, "Ext", "ParentConfigurations.bin")):
mode = get_edit_mode(d)
if mode == "off":
return
msg = ("Конфигурация находится на поддержке (Ext/ParentConfigurations.bin). "
"Правка может быть запрещена.")
if mode == "warn":
print("WARNING: " + msg, file=sys.stderr)
return
die(msg + " Снимите с поддержки (/support-edit) или задайте "
"editingAllowedCheck в .v8-project.json.")
return
parent = os.path.dirname(d)
if parent == d:
break
d = parent
# ── resolve package ──────────────────────────────────────────
package_path = os.path.abspath(args.PackagePath)
pkg_dir = None
if os.path.isdir(package_path):
if os.path.exists(os.path.join(package_path, "Ext", "Package.bin")):
pkg_dir = package_path
elif os.path.isfile(package_path) and os.path.basename(package_path) == "Package.bin":
pkg_dir = os.path.dirname(os.path.dirname(package_path))
elif package_path.endswith(".xml"):
stem = os.path.join(os.path.dirname(package_path),
os.path.splitext(os.path.basename(package_path))[0])
if os.path.exists(os.path.join(stem, "Ext", "Package.bin")):
pkg_dir = stem
if not pkg_dir:
die(f"Не найден пакет XDTO по пути: {package_path}")
pkg_name = os.path.basename(pkg_dir.rstrip("\\/"))
xdto_root = os.path.dirname(pkg_dir)
config_root = os.path.dirname(xdto_root)
bin_file = os.path.join(pkg_dir, "Ext", "Package.bin")
md_file = os.path.join(xdto_root, pkg_name + ".xml")
config_xml = os.path.join(config_root, "Configuration.xml")
# -Value "@путь" — содержимое берётся из файла. Передавать XSD-фрагмент инлайном
# ненадёжно: вложенные кавычки схлопываются на границе процессов, и вместо понятной
# ошибки получается сырой сбой разбора XML.
if args.Value.startswith("@"):
value_file = args.Value[1:]
if not os.path.isabs(value_file):
value_file = os.path.join(os.getcwd(), value_file)
if not os.path.isfile(value_file):
die("Файл значения не найден: " + value_file)
with open(value_file, encoding="utf-8-sig") as f:
args.Value = f.read().strip()
assert_edit_allowed(pkg_dir)
SKILLS = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
DECOMPILE = os.path.join(SKILLS, "xdto-decompile", "scripts", "xdto-decompile.py")
COMPILE = os.path.join(SKILLS, "xdto-compile", "scripts", "xdto-compile.py")
VALIDATE = os.path.join(SKILLS, "xdto-validate", "scripts", "xdto-validate.py")
# Исключение из автономности навыков, сделанное осознанно: конвертер XSD <-> модель
# нельзя скопировать буквально (xdto-compile — скрипт со сквозным потоком, не библиотека),
# а вторая его реализация разошлась бы с первой. Обещание «правка не меняет ни байта
# в нетронутом» держится именно на том, что код тот же самый.
# Проверяем комплектность заранее, чтобы не падать на середине правки.
def assert_siblings_present(operation):
needed = {}
if operation not in ("rename", "set-synonym", "set-comment"):
needed["xdto-decompile"] = DECOMPILE
needed["xdto-compile"] = COMPILE
missing = [k for k, v in needed.items() if not os.path.exists(v)]
if missing:
die("Навык неработоспособен: рядом нет " + ", ".join(missing) + ".\n"
+ f'Операция "{operation}" выполняется через '
+ ("них" if len(missing) > 1 else "него") + ".\n"
+ "Ожидаются в каталоге навыков: " + SKILLS)
def invoke_sibling(script, argv, what):
if not os.path.exists(script):
die(f"Не найден навык {what} по пути: {script}")
r = subprocess.run([sys.executable, script, *argv], capture_output=True, text=True, encoding="utf-8")
if r.returncode != 0:
die(f"{what} завершился с ошибкой:\n{(r.stderr or '') + (r.stdout or '')}")
return r.stdout
def save_xml(doc, path):
raw = etree.tostring(doc, xml_declaration=True, encoding="UTF-8")
with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf" + raw)
# ── metadata object edits ────────────────────────────────────
def edit_metadata(field, new_value):
doc = _parse_xml(md_file)
props = doc.find(f".//{{{MD_NS}}}XDTOPackage/{{{MD_NS}}}Properties")
if props is None:
die("В объекте метаданных не найден блок <Properties>")
if field in ("Name", "Namespace"):
el = props.find(f"{{{MD_NS}}}{field}")
if el is None:
die(f"В объекте метаданных нет <{field}>")
el.text = new_value
elif field == "Comment":
el = props.find(f"{{{MD_NS}}}Comment")
if el is None:
el = etree.SubElement(props, f"{{{MD_NS}}}Comment")
el.text = new_value
elif field == "Synonym":
syn = props.find(f"{{{MD_NS}}}Synonym")
if syn is None:
syn = etree.SubElement(props, f"{{{MD_NS}}}Synonym")
item = None
for it in syn.iterfind(f"{{{V8_NS}}}item"):
lg = it.find(f"{{{V8_NS}}}lang")
if lg is not None and (lg.text or "") == "ru":
item = it
break
if item is None:
item = etree.SubElement(syn, f"{{{V8_NS}}}item")
etree.SubElement(item, f"{{{V8_NS}}}lang").text = "ru"
etree.SubElement(item, f"{{{V8_NS}}}content")
item.find(f"{{{V8_NS}}}content").text = new_value
save_xml(doc, md_file)
def rename_package(new_name):
global pkg_name, pkg_dir
# \w с re.UNICODE уже покрывает кириллицу; явные диапазоны только плодят ошибки
if not re.match(r"^\w+$", new_name, re.UNICODE) or re.match(r"^\d", new_name):
die(f'"{new_name}" не является допустимым идентификатором 1С')
new_md = os.path.join(xdto_root, new_name + ".xml")
new_dir = os.path.join(xdto_root, new_name)
if os.path.exists(new_md) or os.path.exists(new_dir):
die(f'Имя "{new_name}" уже занято другим пакетом')
edit_metadata("Name", new_name)
shutil.move(md_file, new_md)
shutil.move(pkg_dir, new_dir)
if os.path.exists(config_xml):
cfg = _parse_xml(config_xml)
found = False
for e in cfg.iterfind(f".//{{{MD_NS}}}Configuration/{{{MD_NS}}}ChildObjects/{{{MD_NS}}}XDTOPackage"):
if (e.text or "") == pkg_name:
e.text = new_name
found = True
break
if found:
save_xml(cfg, config_xml)
print(f" Configuration.xml: <XDTOPackage> переименован в {new_name}")
else:
print(f"WARNING: В Configuration.xml не найдена запись <XDTOPackage>{pkg_name}</XDTOPackage> — "
"зарегистрируйте пакет вручную", file=sys.stderr)
print(f"✓ Пакет переименован: {pkg_name}{new_name}")
print(f" Перемещены: {new_name}.xml, {new_name}/")
pkg_name = new_name
pkg_dir = new_dir
# ── model edits through the XSD round-trip ───────────────────
def xs_children(el, name):
return [c for c in el if isinstance(c.tag, str)
and etree.QName(c).namespace == XS_NS and local(c) == name]
def xs_first(el, name):
r = xs_children(el, name)
return r[0] if r else None
def find_type_element(schema, type_name):
for kind in ("complexType", "simpleType"):
for t in xs_children(schema, kind):
if t.get("name") == type_name:
return t
die(f'В пакете нет типа "{type_name}"')
def get_type_body(ct):
content = xs_first(ct, "complexContent")
if content is not None:
ext = xs_first(content, "extension")
if ext is not None:
return ext
return ct
def find_declaration(body, prop_name):
for node in body.iter():
if not isinstance(node.tag, str) or etree.QName(node).namespace != XS_NS:
continue
if local(node) in ("element", "attribute") and node.get("name") == prop_name:
return node
return None
def resolve_path(schema, path):
# Точка безопасна: имена в модели XDTO — идентификаторы 1С
segments = path.split(".")
type_el = find_type_element(schema, segments[0])
if len(segments) == 1:
return (type_el, None)
body = get_type_body(type_el)
decl = None
for i in range(1, len(segments)):
decl = find_declaration(body, segments[i])
if decl is None:
die(f'По пути "{path}" не найдено свойство "{segments[i]}"')
if i < len(segments) - 1:
inner = xs_first(decl, "complexType")
if inner is None:
die(f'Свойство "{segments[i]}" не содержит вложенного типа — путь дальше не идёт')
body = get_type_body(inner)
return (type_el, decl)
def import_fragment(schema, xml):
ns = {"xs": XS_NS, "xdto": XDTO_NS}
tns = schema.get("targetNamespace")
if tns:
ns["tns"] = tns
for px, uri in schema.nsmap.items():
if px and px not in ns:
ns[px] = uri
decls = " ".join(f'xmlns:{k}="{v}"' for k, v in ns.items())
try:
wrapped = etree.fromstring(f"<wrap {decls}>{xml}</wrap>".encode("utf-8"))
except etree.XMLSyntaxError as e:
die("Не удалось разобрать -Value как фрагмент XML-схемы: " + str(e) + "\n"
+ "Получено: " + xml + "\n"
+ "Если фрагмент передан инлайном, кавычки могли схлопнуться на границе "
'процессов — положите его в файл и укажите -Value "@путь".')
res = [c for c in wrapped if isinstance(c.tag, str)]
if not res:
die(f"Во фрагменте нет ни одного элемента: {xml}")
return res
def apply_model_operation(schema):
op = args.Operation
if op == "add-property":
if not args.Target:
die("add-property требует -Target <Тип>")
type_el, decl = resolve_path(schema, args.Target)
host = xs_first(decl, "complexType") if decl is not None else type_el
body = get_type_body(host)
for frag in import_fragment(schema, args.Value):
kind = local(frag)
if kind == "attribute":
body.append(frag)
elif kind == "element":
# Явные is not None: пустой <xs:sequence/> в lxml ложен,
# и через "or" мы бы создали вторую частицу
particle = xs_first(body, "sequence")
if particle is None:
particle = xs_first(body, "choice")
if particle is None:
particle = xs_first(body, "all")
if particle is None:
particle = etree.Element(f"{{{XS_NS}}}sequence")
first_attr = xs_first(body, "attribute")
if first_attr is not None:
first_attr.addprevious(particle)
else:
body.append(particle)
particle.append(frag)
else:
die(f"add-property ожидает <xs:element> или <xs:attribute>, получен <xs:{kind}>")
print(f' + {frag.get("name")} в тип {args.Target}')
elif op == "replace-property":
if not args.Target:
die('replace-property требует -Target "Тип.Свойство"')
_, decl = resolve_path(schema, args.Target)
if decl is None:
die('replace-property требует путь вида "Тип.Свойство"')
frags = import_fragment(schema, args.Value)
if len(frags) != 1:
die("replace-property ожидает ровно одно объявление")
decl.getparent().replace(decl, frags[0])
print(f" ~ {args.Target} заменено")
elif op == "remove-property":
if not args.Target:
die('remove-property требует путь "Тип.Свойство"')
for one in [x.strip() for x in args.Target.split(";;") if x.strip()]:
_, decl = resolve_path(schema, one)
if decl is None:
die(f'remove-property требует путь вида "Тип.Свойство", получено "{one}"')
decl.getparent().remove(decl)
print(f" {one} удалено")
elif op == "add-type":
for frag in import_fragment(schema, args.Value):
if local(frag) not in ("complexType", "simpleType"):
die(f"add-type ожидает <xs:complexType> или <xs:simpleType>, получен <xs:{local(frag)}>")
schema.append(frag)
print(f' + тип {frag.get("name")}')
elif op == "remove-type":
if not args.Target:
die("remove-type требует -Target <Тип>")
for one in [x.strip() for x in args.Target.split(";;") if x.strip()]:
t = find_type_element(schema, one)
t.getparent().remove(t)
print(f" тип {one} удалён")
elif op == "add-enum":
if not args.Target:
die("add-enum требует -Target <ТипЗначения>")
t = find_type_element(schema, args.Target)
restriction = xs_first(t, "restriction")
if restriction is None:
die(f'Тип "{args.Target}" не является ограничением простого типа')
for lit in [x.strip() for x in args.Value.split(";;") if x.strip()]:
e = etree.SubElement(restriction, f"{{{XS_NS}}}enumeration")
e.set("value", lit)
print(f' + значение "{lit}" в тип {args.Target}')
elif op == "add-import":
for uri in [x.strip() for x in args.Value.split(";;") if x.strip()]:
if any(i.get("namespace") == uri for i in xs_children(schema, "import")):
print(f" = импорт {uri} уже объявлен")
continue
imp = etree.Element(f"{{{XS_NS}}}import")
imp.set("namespace", uri)
first_other = next((c for c in schema if isinstance(c.tag, str)
and local(c) not in ("annotation", "import")), None)
if first_other is not None:
first_other.addprevious(imp)
else:
schema.append(imp)
print(f" + импорт {uri}")
elif op == "set-namespace":
if not args.Value:
die("set-namespace требует -Value <URI>")
old = schema.get("targetNamespace")
if old == args.Value:
# Установка того же значения не отбрасывается: пакет пересобирается вхолостую
print(f" = namespace уже {args.Value}, пакет пересобран без изменений")
else:
print(f" ~ namespace: {old}{args.Value}")
# Меняем и targetNamespace, и объявление префикса, который на него указывал
new_nsmap = {px: (args.Value if uri == old else uri) for px, uri in schema.nsmap.items()}
rebuilt = etree.Element(schema.tag, nsmap=new_nsmap)
for k, v in schema.attrib.items():
rebuilt.set(k, v)
rebuilt.set("targetNamespace", args.Value)
rebuilt.text = schema.text
for c in list(schema):
rebuilt.append(c)
return rebuilt
return schema
# ── dispatch ─────────────────────────────────────────────────
assert_siblings_present(args.Operation)
print(f"Пакет: {pkg_name}")
old_namespace = None
if args.Operation == "rename":
if not args.Value:
die("rename требует -Value <НовоеИмя>")
rename_package(args.Value)
elif args.Operation == "set-synonym":
if not args.Value:
die("set-synonym требует -Value <текст>")
edit_metadata("Synonym", args.Value)
print(f"✓ Синоним: {args.Value}")
elif args.Operation == "set-comment":
edit_metadata("Comment", args.Value)
print("✓ Комментарий обновлён")
else:
tmp_dir = tempfile.mkdtemp(prefix="xdto-edit_")
try:
xsd_path = os.path.join(tmp_dir, "schema.xsd")
invoke_sibling(DECOMPILE, ["-PackagePath", bin_file, "-OutFile", xsd_path], "xdto-decompile")
doc = _parse_xml(xsd_path)
schema = doc.getroot()
old_namespace = schema.get("targetNamespace")
schema = apply_model_operation(schema)
with open(xsd_path, "wb") as f:
f.write(etree.tostring(schema, xml_declaration=True, encoding="UTF-8"))
invoke_sibling(COMPILE, ["-XsdPath", xsd_path, "-OutputDir", config_root,
"-Name", pkg_name, "-Force"], "xdto-compile")
if args.Operation == "set-namespace":
edit_metadata("Namespace", args.Value)
# Зависящие пакеты не трогаем: при версионировании они обязаны продолжать
# смотреть на прежний namespace. Но молчать о них нельзя.
dependents = []
for d in sorted(os.listdir(xdto_root)):
if d == pkg_name or not os.path.isdir(os.path.join(xdto_root, d)):
continue
ob = os.path.join(xdto_root, d, "Ext", "Package.bin")
if not os.path.exists(ob):
continue
try:
for imp in _parse_xml(ob).getroot():
if isinstance(imp.tag, str) and local(imp) == "import" \
and imp.get("namespace") == old_namespace:
dependents.append(d)
break
except Exception: # noqa: BLE001
pass
if dependents:
print("")
print(f"WARNING: Старый namespace импортируют пакеты ({len(dependents)}): "
+ ", ".join(dependents)
+ ". Они не изменены — при версионировании это верно; "
"если нет, поправьте их импорты.", file=sys.stderr)
print(f"✓ Пакет пересобран: XDTOPackages/{pkg_name}/Ext/Package.bin")
finally:
shutil.rmtree(tmp_dir, ignore_errors=True)
if not args.NoValidate:
if os.path.exists(VALIDATE):
print("")
print("--- xdto-validate ---")
subprocess.run([sys.executable, VALIDATE, "-PackagePath", os.path.join(xdto_root, pkg_name)])
else:
print(f"[SKIP] xdto-validate не найден: {VALIDATE}")
sys.exit(0)