mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-06-10 08:04:56 +03:00
Add subsystem and interface skills (6 new skills)
- subsystem-info: analyze subsystem structure (overview, content, ci, tree modes) - subsystem-compile: create subsystem from JSON definition - subsystem-edit: edit Content, ChildObjects, properties of existing subsystem - subsystem-validate: validate subsystem XML structure (13 checks) - interface-edit: edit CommandInterface.xml (hide/show, place, order) - interface-validate: validate CommandInterface.xml structure (13 checks) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
---
|
||||
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>'
|
||||
```
|
||||
@@ -0,0 +1,48 @@
|
||||
# /interface-edit — редактирование CommandInterface.xml
|
||||
|
||||
Точечное редактирование файла командного интерфейса подсистемы 1С.
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Описание |
|
||||
|----------|----------|
|
||||
| CIPath | Путь к CommandInterface.xml |
|
||||
| DefinitionFile | JSON-файл с массивом операций |
|
||||
| Operation | Одна операция: hide, show, place, order, subsystem-order, group-order |
|
||||
| Value | Значение для операции |
|
||||
| CreateIfMissing | Создать файл если не существует |
|
||||
| NoValidate | Пропустить авто-валидацию |
|
||||
|
||||
## Операции
|
||||
|
||||
| Операция | Значение | Описание |
|
||||
|----------|----------|----------|
|
||||
| hide | Cmd.Name или массив | Скрыть команду (CommandsVisibility, false) |
|
||||
| show | Cmd.Name или массив | Показать команду (visibility, true) |
|
||||
| place | {"command":"...","group":"CommandGroup.X"} | Разместить команду в группе |
|
||||
| order | {"group":"...","commands":[...]} | Задать порядок команд в группе |
|
||||
| subsystem-order | ["Subsystem.X.Subsystem.A",...] | Порядок дочерних подсистем |
|
||||
| group-order | ["NavigationPanelOrdinary",...] | Порядок групп |
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Скрыть команду
|
||||
... -CIPath Subsystems/Продажи/Ext/CommandInterface.xml -Operation hide -Value "Catalog.Товары.StandardCommand.OpenList"
|
||||
|
||||
# Показать команду
|
||||
... -Operation show -Value "Report.Продажи.Command.Отчёт"
|
||||
|
||||
# Разместить в группе
|
||||
... -Operation place -Value '{"command":"Report.X.Command.Y","group":"CommandGroup.Отчеты"}'
|
||||
|
||||
# Задать порядок подсистем
|
||||
... -Operation subsystem-order -Value '["Subsystem.X.Subsystem.A","Subsystem.X.Subsystem.B"]'
|
||||
|
||||
# Создать новый CI
|
||||
... -CIPath <new-path> -Operation subsystem-order -Value '[...]' -CreateIfMissing
|
||||
```
|
||||
|
||||
## Авто-валидация
|
||||
|
||||
После каждой операции автоматически запускается `/interface-validate`. Подавить: `-NoValidate`.
|
||||
@@ -0,0 +1,490 @@
|
||||
# interface-edit v1.0 — Edit 1C CommandInterface.xml
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$CIPath,
|
||||
[string]$DefinitionFile,
|
||||
[ValidateSet("hide","show","place","order","subsystem-order","group-order")]
|
||||
[string]$Operation,
|
||||
[string]$Value,
|
||||
[switch]$CreateIfMissing,
|
||||
[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($CIPath)) {
|
||||
$CIPath = Join-Path (Get-Location).Path $CIPath
|
||||
}
|
||||
$resolvedPath = $CIPath
|
||||
|
||||
# --- Namespaces ---
|
||||
$script:ciNs = "http://v8.1c.ru/8.3/xcf/extrnprops"
|
||||
$script:xrNs = "http://v8.1c.ru/8.3/xcf/readable"
|
||||
$script:xsiNs = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
$script:xsNs = "http://www.w3.org/2001/XMLSchema"
|
||||
|
||||
# --- Create if missing ---
|
||||
if (-not (Test-Path $CIPath)) {
|
||||
if ($CreateIfMissing) {
|
||||
$parentDir = [System.IO.Path]::GetDirectoryName($CIPath)
|
||||
if (-not (Test-Path $parentDir)) {
|
||||
New-Item -ItemType Directory -Path $parentDir -Force | Out-Null
|
||||
}
|
||||
$emptyCI = @"
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<CommandInterface xmlns="$($script:ciNs)"
|
||||
xmlns:xr="$($script:xrNs)"
|
||||
xmlns:xs="$($script:xsNs)"
|
||||
xmlns:xsi="$($script:xsiNs)"
|
||||
version="2.17">
|
||||
</CommandInterface>
|
||||
"@
|
||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||
[System.IO.File]::WriteAllText($CIPath, $emptyCI, $utf8Bom)
|
||||
Write-Host "[INFO] Created new CommandInterface.xml: $CIPath"
|
||||
} else {
|
||||
Write-Error "File not found: $CIPath (use -CreateIfMissing to create)"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
$resolvedPath = (Resolve-Path $CIPath).Path
|
||||
|
||||
# --- Load XML ---
|
||||
$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
|
||||
if ($root.LocalName -ne "CommandInterface") {
|
||||
Write-Error "Expected <CommandInterface> root element, got <$($root.LocalName)>"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Section canonical order
|
||||
$script:sectionOrder = @("CommandsVisibility","CommandsPlacement","CommandsOrder","SubsystemsOrder","GroupsOrder")
|
||||
|
||||
# --- XML manipulation helpers ---
|
||||
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 Import-CIFragment([string]$xmlString) {
|
||||
$wrapper = "<_W xmlns=`"$($script:ciNs)`" xmlns:xr=`"$($script:xrNs)`" xmlns:xsi=`"$($script:xsiNs)`" xmlns:xs=`"$($script:xsNs)`">$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
|
||||
}
|
||||
|
||||
# --- Ensure section exists, creating it in correct order if needed ---
|
||||
function Ensure-Section([string]$sectionName) {
|
||||
# Find existing
|
||||
foreach ($child in $root.ChildNodes) {
|
||||
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $sectionName) {
|
||||
return $child
|
||||
}
|
||||
}
|
||||
|
||||
# Create new section
|
||||
$newSection = $script:xmlDoc.CreateElement($sectionName, $script:ciNs)
|
||||
|
||||
# Find the correct insertion point: before the first section that comes AFTER us in canonical order
|
||||
$myIdx = [array]::IndexOf($script:sectionOrder, $sectionName)
|
||||
$refNode = $null
|
||||
foreach ($child in $root.ChildNodes) {
|
||||
if ($child.NodeType -ne 'Element') { continue }
|
||||
$childIdx = [array]::IndexOf($script:sectionOrder, $child.LocalName)
|
||||
if ($childIdx -gt $myIdx) {
|
||||
# Find the whitespace before this element to insert before it
|
||||
$prev = $child.PreviousSibling
|
||||
if ($prev -and ($prev.NodeType -eq 'Whitespace' -or $prev.NodeType -eq 'SignificantWhitespace')) {
|
||||
$refNode = $prev
|
||||
} else {
|
||||
$refNode = $child
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
$rootIndent = Get-ChildIndent $root
|
||||
# Add closing whitespace inside the new section
|
||||
$closeWs = $script:xmlDoc.CreateWhitespace("`r`n$rootIndent")
|
||||
$newSection.AppendChild($closeWs) | Out-Null
|
||||
|
||||
if ($refNode) {
|
||||
$ws = $script:xmlDoc.CreateWhitespace("`r`n$rootIndent")
|
||||
$root.InsertBefore($ws, $refNode) | Out-Null
|
||||
$root.InsertBefore($newSection, $ws) | Out-Null
|
||||
} else {
|
||||
Insert-BeforeElement $root $newSection $null $rootIndent
|
||||
}
|
||||
return $newSection
|
||||
}
|
||||
|
||||
# --- 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)
|
||||
}
|
||||
|
||||
# --- Find Command element by name in a section ---
|
||||
function Find-CommandByName($section, [string]$cmdName) {
|
||||
foreach ($child in $section.ChildNodes) {
|
||||
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Command") {
|
||||
if ($child.GetAttribute("name") -eq $cmdName) { return $child }
|
||||
}
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
# --- Operations ---
|
||||
|
||||
function Do-Hide([string[]]$commands) {
|
||||
$section = Ensure-Section "CommandsVisibility"
|
||||
$sectionIndent = Get-ChildIndent $section
|
||||
|
||||
foreach ($cmd in $commands) {
|
||||
$existing = Find-CommandByName $section $cmd
|
||||
if ($existing) {
|
||||
# Check if already false
|
||||
$commonEl = $null
|
||||
foreach ($vis in $existing.ChildNodes) {
|
||||
if ($vis.NodeType -eq 'Element' -and $vis.LocalName -eq "Visibility") {
|
||||
foreach ($c in $vis.ChildNodes) {
|
||||
if ($c.NodeType -eq 'Element' -and $c.LocalName -eq "Common") { $commonEl = $c; break }
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($commonEl -and $commonEl.InnerText.Trim() -eq "false") {
|
||||
Warn "Already hidden: $cmd"
|
||||
continue
|
||||
}
|
||||
# Change true -> false
|
||||
if ($commonEl) {
|
||||
$commonEl.InnerText = "false"
|
||||
$script:modifyCount++
|
||||
Info "Changed to hidden: $cmd"
|
||||
continue
|
||||
}
|
||||
}
|
||||
# Add new entry
|
||||
$fragXml = "<Command name=`"$cmd`"><Visibility><xr:Common>false</xr:Common></Visibility></Command>"
|
||||
$nodes = Import-CIFragment $fragXml
|
||||
if ($nodes.Count -gt 0) {
|
||||
Insert-BeforeElement $section $nodes[0] $null $sectionIndent
|
||||
$script:addCount++
|
||||
Info "Hidden: $cmd"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Do-Show([string[]]$commands) {
|
||||
$section = $null
|
||||
foreach ($child in $root.ChildNodes) {
|
||||
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "CommandsVisibility") {
|
||||
$section = $child; break
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($cmd in $commands) {
|
||||
if (-not $section) {
|
||||
# No CommandsVisibility section — showing means adding with true
|
||||
$section = Ensure-Section "CommandsVisibility"
|
||||
}
|
||||
$existing = Find-CommandByName $section $cmd
|
||||
if ($existing) {
|
||||
$commonEl = $null
|
||||
foreach ($vis in $existing.ChildNodes) {
|
||||
if ($vis.NodeType -eq 'Element' -and $vis.LocalName -eq "Visibility") {
|
||||
foreach ($c in $vis.ChildNodes) {
|
||||
if ($c.NodeType -eq 'Element' -and $c.LocalName -eq "Common") { $commonEl = $c; break }
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($commonEl -and $commonEl.InnerText.Trim() -eq "true") {
|
||||
Warn "Already shown: $cmd"
|
||||
continue
|
||||
}
|
||||
if ($commonEl -and $commonEl.InnerText.Trim() -eq "false") {
|
||||
# Change false -> true
|
||||
$commonEl.InnerText = "true"
|
||||
$script:modifyCount++
|
||||
Info "Changed to shown: $cmd"
|
||||
continue
|
||||
}
|
||||
}
|
||||
# Add new entry with true
|
||||
$sectionIndent = Get-ChildIndent $section
|
||||
$fragXml = "<Command name=`"$cmd`"><Visibility><xr:Common>true</xr:Common></Visibility></Command>"
|
||||
$nodes = Import-CIFragment $fragXml
|
||||
if ($nodes.Count -gt 0) {
|
||||
Insert-BeforeElement $section $nodes[0] $null $sectionIndent
|
||||
$script:addCount++
|
||||
Info "Shown: $cmd"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Do-Place([string]$jsonVal) {
|
||||
$def = $jsonVal | ConvertFrom-Json
|
||||
$cmdName = "$($def.command)"
|
||||
$groupName = "$($def.group)"
|
||||
if (-not $cmdName -or -not $groupName) { Write-Error "place requires {command, group}"; exit 1 }
|
||||
|
||||
$section = Ensure-Section "CommandsPlacement"
|
||||
$sectionIndent = Get-ChildIndent $section
|
||||
|
||||
# Check existing
|
||||
$existing = Find-CommandByName $section $cmdName
|
||||
if ($existing) {
|
||||
# Update group
|
||||
foreach ($child in $existing.ChildNodes) {
|
||||
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "CommandGroup") {
|
||||
$child.InnerText = $groupName
|
||||
$script:modifyCount++
|
||||
Info "Updated placement: $cmdName -> $groupName"
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Add new
|
||||
$fragXml = "<Command name=`"$cmdName`"><CommandGroup>$groupName</CommandGroup><Placement>Auto</Placement></Command>"
|
||||
$nodes = Import-CIFragment $fragXml
|
||||
if ($nodes.Count -gt 0) {
|
||||
Insert-BeforeElement $section $nodes[0] $null $sectionIndent
|
||||
$script:addCount++
|
||||
Info "Placed: $cmdName -> $groupName"
|
||||
}
|
||||
}
|
||||
|
||||
function Do-Order([string]$jsonVal) {
|
||||
$def = $jsonVal | ConvertFrom-Json
|
||||
$groupName = "$($def.group)"
|
||||
$commands = @($def.commands | ForEach-Object { "$_" })
|
||||
if (-not $groupName -or $commands.Count -eq 0) { Write-Error "order requires {group, commands:[...]}"; exit 1 }
|
||||
|
||||
$section = Ensure-Section "CommandsOrder"
|
||||
$sectionIndent = Get-ChildIndent $section
|
||||
|
||||
# Remove existing entries for this group
|
||||
$toRemove = @()
|
||||
foreach ($child in $section.ChildNodes) {
|
||||
if ($child.NodeType -ne 'Element') { continue }
|
||||
if ($child.LocalName -ne "Command") { continue }
|
||||
foreach ($gc in $child.ChildNodes) {
|
||||
if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "CommandGroup" -and $gc.InnerText.Trim() -eq $groupName) {
|
||||
$toRemove += $child
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($node in $toRemove) {
|
||||
Remove-NodeWithWhitespace $node
|
||||
$script:removeCount++
|
||||
}
|
||||
|
||||
# Add new entries in order
|
||||
foreach ($cmdName in $commands) {
|
||||
$fragXml = "<Command name=`"$cmdName`"><CommandGroup>$groupName</CommandGroup></Command>"
|
||||
$nodes = Import-CIFragment $fragXml
|
||||
if ($nodes.Count -gt 0) {
|
||||
Insert-BeforeElement $section $nodes[0] $null $sectionIndent
|
||||
$script:addCount++
|
||||
}
|
||||
}
|
||||
Info "Set order for $groupName : $($commands.Count) commands"
|
||||
}
|
||||
|
||||
function Do-SubsystemOrder([string]$jsonVal) {
|
||||
$parsed = $jsonVal | ConvertFrom-Json
|
||||
$subsystems = @(); foreach ($s in $parsed) { $subsystems += "$s" }
|
||||
if ($subsystems.Count -eq 0) { Write-Error "subsystem-order requires array of subsystem paths"; exit 1 }
|
||||
|
||||
$section = Ensure-Section "SubsystemsOrder"
|
||||
$sectionIndent = Get-ChildIndent $section
|
||||
|
||||
# Clear existing
|
||||
$toRemove = @()
|
||||
foreach ($child in @($section.ChildNodes)) {
|
||||
if ($child.NodeType -eq 'Element') { $toRemove += $child }
|
||||
}
|
||||
foreach ($node in $toRemove) {
|
||||
Remove-NodeWithWhitespace $node
|
||||
$script:removeCount++
|
||||
}
|
||||
|
||||
# Add new entries
|
||||
foreach ($sub in $subsystems) {
|
||||
$newEl = $script:xmlDoc.CreateElement("Subsystem", $script:ciNs)
|
||||
$newEl.InnerText = $sub
|
||||
Insert-BeforeElement $section $newEl $null $sectionIndent
|
||||
$script:addCount++
|
||||
}
|
||||
Info "Set subsystem order: $($subsystems.Count) entries"
|
||||
}
|
||||
|
||||
function Do-GroupOrder([string]$jsonVal) {
|
||||
$parsed = $jsonVal | ConvertFrom-Json
|
||||
$groups = @(); foreach ($g in $parsed) { $groups += "$g" }
|
||||
if ($groups.Count -eq 0) { Write-Error "group-order requires array of group names"; exit 1 }
|
||||
|
||||
$section = Ensure-Section "GroupsOrder"
|
||||
$sectionIndent = Get-ChildIndent $section
|
||||
|
||||
# Clear existing
|
||||
$toRemove = @()
|
||||
foreach ($child in @($section.ChildNodes)) {
|
||||
if ($child.NodeType -eq 'Element') { $toRemove += $child }
|
||||
}
|
||||
foreach ($node in $toRemove) {
|
||||
Remove-NodeWithWhitespace $node
|
||||
$script:removeCount++
|
||||
}
|
||||
|
||||
# Add new entries
|
||||
foreach ($grp in $groups) {
|
||||
$newEl = $script:xmlDoc.CreateElement("Group", $script:ciNs)
|
||||
$newEl.InnerText = $grp
|
||||
Insert-BeforeElement $section $newEl $null $sectionIndent
|
||||
$script:addCount++
|
||||
}
|
||||
Info "Set group order: $($groups.Count) entries"
|
||||
}
|
||||
|
||||
# --- 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) {
|
||||
"hide" { Do-Hide (Parse-ValueList $opValue) }
|
||||
"show" { Do-Show (Parse-ValueList $opValue) }
|
||||
"place" { Do-Place $opValue }
|
||||
"order" { Do-Order $opValue }
|
||||
"subsystem-order" { Do-SubsystemOrder $opValue }
|
||||
"group-order" { Do-GroupOrder $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 "..\..\interface-validate") "scripts\interface-validate.ps1"
|
||||
$validateScript = [System.IO.Path]::GetFullPath($validateScript)
|
||||
if (Test-Path $validateScript) {
|
||||
Write-Host ""
|
||||
Write-Host "--- Running interface-validate ---"
|
||||
& powershell.exe -NoProfile -File $validateScript -CIPath $resolvedPath
|
||||
}
|
||||
}
|
||||
|
||||
# --- Summary ---
|
||||
Write-Host ""
|
||||
Write-Host "=== interface-edit summary ==="
|
||||
Write-Host " Added: $($script:addCount)"
|
||||
Write-Host " Removed: $($script:removeCount)"
|
||||
Write-Host " Modified: $($script:modifyCount)"
|
||||
exit 0
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
name: interface-validate
|
||||
description: Валидация структурной корректности командного интерфейса 1С (CommandInterface.xml) — формат, секции, ссылки на команды, дубликаты
|
||||
argument-hint: <CIPath> [-MaxErrors 30]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /interface-validate — валидация CommandInterface.xml
|
||||
|
||||
Проверяет XML командного интерфейса из выгрузки конфигурации на структурные ошибки: корневой элемент, допустимые секции, порядок, формат ссылок на команды, дубликаты.
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Обязательный | По умолчанию | Описание |
|
||||
|-----------|:------------:|--------------|------------------------------------|
|
||||
| CIPath | да | — | Путь к CommandInterface.xml |
|
||||
| MaxErrors | нет | 30 | Остановиться после N ошибок |
|
||||
| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) |
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File '.claude\skills\interface-validate\scripts\interface-validate.ps1' -CIPath '<path>'
|
||||
```
|
||||
|
||||
## Проверки (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 |
|
||||
|
||||
## Вывод
|
||||
|
||||
```
|
||||
=== Validation: CommandInterface (Продажи) ===
|
||||
|
||||
[OK] 1. Root structure: CommandInterface, version 2.17, namespace valid
|
||||
[OK] 2. Child elements: 5 valid sections
|
||||
[OK] 3. Section order: correct
|
||||
[OK] 4. No duplicate sections
|
||||
[OK] 5. CommandsVisibility: 55 entries, all valid
|
||||
[OK] 6. CommandsVisibility: no duplicates
|
||||
[OK] 7. CommandsPlacement: 3 entries, all valid
|
||||
[OK] 8. CommandsOrder: 12 entries, all valid
|
||||
[OK] 9. SubsystemsOrder: 9 entries, all valid format
|
||||
[OK] 10. SubsystemsOrder: no duplicates
|
||||
[OK] 11. GroupsOrder: 7 entries, all valid
|
||||
[OK] 12. GroupsOrder: no duplicates
|
||||
[OK] 13. Command reference format: all valid
|
||||
---
|
||||
Errors: 0, Warnings: 0
|
||||
```
|
||||
|
||||
Код возврата: 0 = все проверки пройдены, 1 = есть ошибки.
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# CommandInterface подсистемы
|
||||
... -CIPath upload/acc_8.3.24/Subsystems/Продажи/Ext/CommandInterface.xml
|
||||
|
||||
# Корневой CommandInterface конфигурации
|
||||
... -CIPath upload/acc_8.3.24/Ext/CommandInterface.xml
|
||||
|
||||
# С лимитом ошибок
|
||||
... -CIPath <path> -MaxErrors 10
|
||||
```
|
||||
@@ -0,0 +1,382 @@
|
||||
# interface-validate v1.0 — Validate 1C CommandInterface.xml structure
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$CIPath,
|
||||
[int]$MaxErrors = 30,
|
||||
[string]$OutFile
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve path ---
|
||||
if (-not [System.IO.Path]::IsPathRooted($CIPath)) {
|
||||
$CIPath = Join-Path (Get-Location).Path $CIPath
|
||||
}
|
||||
if (-not (Test-Path $CIPath)) {
|
||||
Write-Host "[ERROR] File not found: $CIPath"
|
||||
exit 1
|
||||
}
|
||||
$resolvedPath = (Resolve-Path $CIPath).Path
|
||||
|
||||
# --- Derive context name from path ---
|
||||
$contextName = ""
|
||||
$parentParts = $resolvedPath -split '[/\\]'
|
||||
for ($i = 0; $i -lt $parentParts.Count; $i++) {
|
||||
if ($parentParts[$i] -eq "Subsystems" -and ($i + 1) -lt $parentParts.Count) {
|
||||
$contextName = $parentParts[$i + 1]
|
||||
}
|
||||
}
|
||||
if (-not $contextName) { $contextName = "Root" }
|
||||
|
||||
# --- Output infrastructure ---
|
||||
$script:errors = 0
|
||||
$script:warnings = 0
|
||||
$script:stopped = $false
|
||||
$script:output = New-Object System.Text.StringBuilder 8192
|
||||
$script:allCommandNames = @()
|
||||
|
||||
function Out-Line([string]$msg) { $script:output.AppendLine($msg) | Out-Null }
|
||||
function Report-OK([string]$msg) { Out-Line "[OK] $msg" }
|
||||
function Report-Error([string]$msg) {
|
||||
$script:errors++
|
||||
Out-Line "[ERROR] $msg"
|
||||
if ($script:errors -ge $MaxErrors) { $script:stopped = $true }
|
||||
}
|
||||
function Report-Warn([string]$msg) {
|
||||
$script:warnings++
|
||||
Out-Line "[WARN] $msg"
|
||||
}
|
||||
|
||||
Out-Line "=== Validation: CommandInterface ($contextName) ==="
|
||||
Out-Line ""
|
||||
|
||||
# --- Valid section names and order ---
|
||||
$validSections = @("CommandsVisibility","CommandsPlacement","CommandsOrder","SubsystemsOrder","GroupsOrder")
|
||||
|
||||
# Command reference patterns
|
||||
$stdCmdPattern = '^[A-Za-z]+\.[^\s\.]+\.StandardCommand\.\w+$'
|
||||
$customCmdPattern = '^[A-Za-z]+\.[^\s\.]+\.Command\.\w+$'
|
||||
$commonCmdPattern = '^CommonCommand\.\w+$'
|
||||
$uuidCmdPattern = '^0:[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}$'
|
||||
|
||||
# --- 1. XML well-formedness + root structure ---
|
||||
$xmlDoc = $null
|
||||
try {
|
||||
[xml]$xmlDoc = Get-Content -Path $resolvedPath -Encoding UTF8
|
||||
} catch {
|
||||
Report-Error "1. XML parse error: $($_.Exception.Message)"
|
||||
$script:stopped = $true
|
||||
}
|
||||
|
||||
if (-not $script:stopped) {
|
||||
$root = $xmlDoc.DocumentElement
|
||||
|
||||
if ($root.LocalName -ne "CommandInterface") {
|
||||
Report-Error "1. Root element: expected <CommandInterface>, got <$($root.LocalName)>"
|
||||
$script:stopped = $true
|
||||
} else {
|
||||
$nsUri = $root.NamespaceURI
|
||||
$version = $root.GetAttribute("version")
|
||||
$expectedNs = "http://v8.1c.ru/8.3/xcf/extrnprops"
|
||||
if ($nsUri -ne $expectedNs) {
|
||||
Report-Error "1. Root namespace: expected $expectedNs, got $nsUri"
|
||||
} elseif (-not $version) {
|
||||
Report-Warn "1. Root structure: CommandInterface, namespace valid, but no version attribute"
|
||||
} else {
|
||||
Report-OK "1. Root structure: CommandInterface, version $version, namespace valid"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Setup namespace manager ---
|
||||
$ns = $null
|
||||
if (-not $script:stopped) {
|
||||
$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
|
||||
$ns.AddNamespace("ci", "http://v8.1c.ru/8.3/xcf/extrnprops")
|
||||
$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable")
|
||||
$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance")
|
||||
$ns.AddNamespace("xs", "http://www.w3.org/2001/XMLSchema")
|
||||
}
|
||||
|
||||
# --- 2. Valid child elements ---
|
||||
if (-not $script:stopped) {
|
||||
$root = $xmlDoc.DocumentElement
|
||||
$foundSections = @()
|
||||
$invalidElements = @()
|
||||
foreach ($child in $root.ChildNodes) {
|
||||
if ($child.NodeType -ne 'Element') { continue }
|
||||
if ($child.LocalName -in $validSections) {
|
||||
$foundSections += $child.LocalName
|
||||
} else {
|
||||
$invalidElements += $child.LocalName
|
||||
}
|
||||
}
|
||||
if ($invalidElements.Count -gt 0) {
|
||||
Report-Error "2. Invalid child elements: $($invalidElements -join ', ')"
|
||||
} else {
|
||||
Report-OK "2. Child elements: $($foundSections.Count) valid sections"
|
||||
}
|
||||
}
|
||||
|
||||
# --- 3. Section order ---
|
||||
if (-not $script:stopped) {
|
||||
$orderOk = $true
|
||||
$lastIdx = -1
|
||||
foreach ($sec in $foundSections) {
|
||||
$idx = [array]::IndexOf($validSections, $sec)
|
||||
if ($idx -lt $lastIdx) {
|
||||
Report-Error "3. Section order: '$sec' appears after a later section (expected: CommandsVisibility -> CommandsPlacement -> CommandsOrder -> SubsystemsOrder -> GroupsOrder)"
|
||||
$orderOk = $false
|
||||
break
|
||||
}
|
||||
$lastIdx = $idx
|
||||
}
|
||||
if ($orderOk) { Report-OK "3. Section order: correct" }
|
||||
}
|
||||
|
||||
# --- 4. No duplicate sections ---
|
||||
if (-not $script:stopped) {
|
||||
$dupes = $foundSections | Group-Object | Where-Object { $_.Count -gt 1 }
|
||||
if ($dupes) {
|
||||
$dupeNames = ($dupes | ForEach-Object { $_.Name }) -join ", "
|
||||
Report-Error "4. Duplicate sections: $dupeNames"
|
||||
} else {
|
||||
Report-OK "4. No duplicate sections"
|
||||
}
|
||||
}
|
||||
|
||||
# --- 5. CommandsVisibility ---
|
||||
if (-not $script:stopped) {
|
||||
$visSection = $root.SelectSingleNode("ci:CommandsVisibility", $ns)
|
||||
if ($visSection) {
|
||||
$visOk = $true
|
||||
$visNames = @()
|
||||
$visCount = 0
|
||||
foreach ($cmd in $visSection.ChildNodes) {
|
||||
if ($cmd.NodeType -ne 'Element') { continue }
|
||||
$visCount++
|
||||
$cmdName = $cmd.GetAttribute("name")
|
||||
if (-not $cmdName) {
|
||||
Report-Error "5. CommandsVisibility: Command element without 'name' attribute"
|
||||
$visOk = $false; continue
|
||||
}
|
||||
$visNames += $cmdName
|
||||
$script:allCommandNames += $cmdName
|
||||
$visibility = $cmd.SelectSingleNode("ci:Visibility", $ns)
|
||||
if (-not $visibility) {
|
||||
Report-Error "5. CommandsVisibility[$cmdName]: missing <Visibility>"
|
||||
$visOk = $false; continue
|
||||
}
|
||||
$common = $visibility.SelectSingleNode("xr:Common", $ns)
|
||||
if (-not $common) {
|
||||
Report-Error "5. CommandsVisibility[$cmdName]: missing <xr:Common>"
|
||||
$visOk = $false; continue
|
||||
}
|
||||
$val = $common.InnerText.Trim()
|
||||
if ($val -ne "true" -and $val -ne "false") {
|
||||
Report-Error "5. CommandsVisibility[$cmdName]: xr:Common='$val' (expected true/false)"
|
||||
$visOk = $false
|
||||
}
|
||||
}
|
||||
if ($visOk) { Report-OK "5. CommandsVisibility: $visCount entries, all valid" }
|
||||
} else {
|
||||
Report-OK "5. CommandsVisibility: not present"
|
||||
$visNames = @()
|
||||
}
|
||||
}
|
||||
|
||||
# --- 6. CommandsVisibility duplicates ---
|
||||
if (-not $script:stopped) {
|
||||
if ($visNames.Count -gt 0) {
|
||||
$dupes = $visNames | Group-Object | Where-Object { $_.Count -gt 1 }
|
||||
if ($dupes) {
|
||||
$dupeNames = ($dupes | ForEach-Object { $_.Name }) -join ", "
|
||||
Report-Warn "6. CommandsVisibility: duplicates: $dupeNames"
|
||||
} else {
|
||||
Report-OK "6. CommandsVisibility: no duplicates"
|
||||
}
|
||||
} else {
|
||||
Report-OK "6. CommandsVisibility: no duplicates (empty)"
|
||||
}
|
||||
}
|
||||
|
||||
# --- 7. CommandsPlacement ---
|
||||
if (-not $script:stopped) {
|
||||
$plcSection = $root.SelectSingleNode("ci:CommandsPlacement", $ns)
|
||||
if ($plcSection) {
|
||||
$plcOk = $true
|
||||
$plcCount = 0
|
||||
foreach ($cmd in $plcSection.ChildNodes) {
|
||||
if ($cmd.NodeType -ne 'Element') { continue }
|
||||
$plcCount++
|
||||
$cmdName = $cmd.GetAttribute("name")
|
||||
if (-not $cmdName) {
|
||||
Report-Error "7. CommandsPlacement: Command without 'name' attribute"
|
||||
$plcOk = $false; continue
|
||||
}
|
||||
$script:allCommandNames += $cmdName
|
||||
$grpEl = $cmd.SelectSingleNode("ci:CommandGroup", $ns)
|
||||
if (-not $grpEl -or -not $grpEl.InnerText.Trim()) {
|
||||
Report-Error "7. CommandsPlacement[$cmdName]: missing or empty <CommandGroup>"
|
||||
$plcOk = $false; continue
|
||||
}
|
||||
$placementEl = $cmd.SelectSingleNode("ci:Placement", $ns)
|
||||
if (-not $placementEl) {
|
||||
Report-Error "7. CommandsPlacement[$cmdName]: missing <Placement>"
|
||||
$plcOk = $false
|
||||
} elseif ($placementEl.InnerText.Trim() -ne "Auto") {
|
||||
Report-Warn "7. CommandsPlacement[$cmdName]: Placement='$($placementEl.InnerText.Trim())' (expected Auto)"
|
||||
}
|
||||
}
|
||||
if ($plcOk) { Report-OK "7. CommandsPlacement: $plcCount entries, all valid" }
|
||||
} else {
|
||||
Report-OK "7. CommandsPlacement: not present"
|
||||
}
|
||||
}
|
||||
|
||||
# --- 8. CommandsOrder ---
|
||||
if (-not $script:stopped) {
|
||||
$ordSection = $root.SelectSingleNode("ci:CommandsOrder", $ns)
|
||||
if ($ordSection) {
|
||||
$ordOk = $true
|
||||
$ordCount = 0
|
||||
foreach ($cmd in $ordSection.ChildNodes) {
|
||||
if ($cmd.NodeType -ne 'Element') { continue }
|
||||
$ordCount++
|
||||
$cmdName = $cmd.GetAttribute("name")
|
||||
if (-not $cmdName) {
|
||||
Report-Error "8. CommandsOrder: Command without 'name' attribute"
|
||||
$ordOk = $false; continue
|
||||
}
|
||||
$script:allCommandNames += $cmdName
|
||||
$grpEl = $cmd.SelectSingleNode("ci:CommandGroup", $ns)
|
||||
if (-not $grpEl -or -not $grpEl.InnerText.Trim()) {
|
||||
Report-Error "8. CommandsOrder[$cmdName]: missing or empty <CommandGroup>"
|
||||
$ordOk = $false
|
||||
}
|
||||
}
|
||||
if ($ordOk) { Report-OK "8. CommandsOrder: $ordCount entries, all valid" }
|
||||
} else {
|
||||
Report-OK "8. CommandsOrder: not present"
|
||||
}
|
||||
}
|
||||
|
||||
# --- 9. SubsystemsOrder format ---
|
||||
if (-not $script:stopped) {
|
||||
$subSection = $root.SelectSingleNode("ci:SubsystemsOrder", $ns)
|
||||
$subNames = @()
|
||||
if ($subSection) {
|
||||
$subOk = $true
|
||||
$subCount = 0
|
||||
foreach ($sub in $subSection.ChildNodes) {
|
||||
if ($sub.NodeType -ne 'Element') { continue }
|
||||
$subCount++
|
||||
$text = $sub.InnerText.Trim()
|
||||
$subNames += $text
|
||||
if (-not $text) {
|
||||
Report-Error "9. SubsystemsOrder: empty <Subsystem> element"
|
||||
$subOk = $false
|
||||
} elseif ($text -notmatch '^Subsystem\.') {
|
||||
Report-Error "9. SubsystemsOrder: '$text' - expected format Subsystem.X..."
|
||||
$subOk = $false
|
||||
}
|
||||
}
|
||||
if ($subOk) { Report-OK "9. SubsystemsOrder: $subCount entries, all valid format" }
|
||||
} else {
|
||||
Report-OK "9. SubsystemsOrder: not present"
|
||||
}
|
||||
}
|
||||
|
||||
# --- 10. SubsystemsOrder duplicates ---
|
||||
if (-not $script:stopped) {
|
||||
if ($subNames.Count -gt 0) {
|
||||
$dupes = $subNames | Group-Object | Where-Object { $_.Count -gt 1 }
|
||||
if ($dupes) {
|
||||
$dupeNames = ($dupes | ForEach-Object { $_.Name }) -join ", "
|
||||
Report-Warn "10. SubsystemsOrder: duplicates: $dupeNames"
|
||||
} else {
|
||||
Report-OK "10. SubsystemsOrder: no duplicates"
|
||||
}
|
||||
} else {
|
||||
Report-OK "10. SubsystemsOrder: no duplicates (empty)"
|
||||
}
|
||||
}
|
||||
|
||||
# --- 11. GroupsOrder entries ---
|
||||
if (-not $script:stopped) {
|
||||
$grpSection = $root.SelectSingleNode("ci:GroupsOrder", $ns)
|
||||
$grpNames = @()
|
||||
if ($grpSection) {
|
||||
$grpOk = $true
|
||||
$grpCount = 0
|
||||
foreach ($grp in $grpSection.ChildNodes) {
|
||||
if ($grp.NodeType -ne 'Element') { continue }
|
||||
$grpCount++
|
||||
$text = $grp.InnerText.Trim()
|
||||
$grpNames += $text
|
||||
if (-not $text) {
|
||||
Report-Error "11. GroupsOrder: empty <Group> element"
|
||||
$grpOk = $false
|
||||
}
|
||||
}
|
||||
if ($grpOk) { Report-OK "11. GroupsOrder: $grpCount entries, all valid" }
|
||||
} else {
|
||||
Report-OK "11. GroupsOrder: not present"
|
||||
}
|
||||
}
|
||||
|
||||
# --- 12. GroupsOrder duplicates ---
|
||||
if (-not $script:stopped) {
|
||||
if ($grpNames.Count -gt 0) {
|
||||
$dupes = $grpNames | Group-Object | Where-Object { $_.Count -gt 1 }
|
||||
if ($dupes) {
|
||||
$dupeNames = ($dupes | ForEach-Object { $_.Name }) -join ", "
|
||||
Report-Warn "12. GroupsOrder: duplicates: $dupeNames"
|
||||
} else {
|
||||
Report-OK "12. GroupsOrder: no duplicates"
|
||||
}
|
||||
} else {
|
||||
Report-OK "12. GroupsOrder: no duplicates (empty)"
|
||||
}
|
||||
}
|
||||
|
||||
# --- 13. Command reference format ---
|
||||
if (-not $script:stopped) {
|
||||
if ($script:allCommandNames.Count -gt 0) {
|
||||
$badRefs = @()
|
||||
foreach ($ref in $script:allCommandNames) {
|
||||
if ($ref -match $stdCmdPattern) { continue }
|
||||
if ($ref -match $customCmdPattern) { continue }
|
||||
if ($ref -match $commonCmdPattern) { continue }
|
||||
if ($ref -match $uuidCmdPattern) { continue }
|
||||
$badRefs += $ref
|
||||
}
|
||||
if ($badRefs.Count -eq 0) {
|
||||
Report-OK "13. Command reference format: all $($script:allCommandNames.Count) valid"
|
||||
} else {
|
||||
$shown = $badRefs[0..([Math]::Min(4, $badRefs.Count - 1))]
|
||||
Report-Warn "13. Command reference format: $($badRefs.Count) unrecognized: $($shown -join ', ')$(if($badRefs.Count -gt 5){' ...'})"
|
||||
}
|
||||
} else {
|
||||
Report-OK "13. Command reference format: n/a (no commands)"
|
||||
}
|
||||
}
|
||||
|
||||
# --- Finalize ---
|
||||
Out-Line "---"
|
||||
Out-Line "Errors: $($script:errors), Warnings: $($script:warnings)"
|
||||
|
||||
$result = $script:output.ToString()
|
||||
Write-Host $result
|
||||
|
||||
if ($OutFile) {
|
||||
if (-not [System.IO.Path]::IsPathRooted($OutFile)) {
|
||||
$OutFile = Join-Path (Get-Location).Path $OutFile
|
||||
}
|
||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||
[System.IO.File]::WriteAllText($OutFile, $result, $utf8Bom)
|
||||
Write-Host "Written to: $OutFile"
|
||||
}
|
||||
|
||||
if ($script:errors -gt 0) { exit 1 } else { exit 0 }
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
name: subsystem-compile
|
||||
description: Создать подсистему 1С — XML-исходники из JSON-определения. Используй когда пользователь просит добавить подсистему (раздел) в конфигурацию
|
||||
argument-hint: [-DefinitionFile <json> | -Value <json-string>] -OutputDir <ConfigDir> [-Parent <path>]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Write
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /subsystem-compile — генерация подсистемы из JSON
|
||||
|
||||
Принимает JSON-определение подсистемы → генерирует XML + файловую структуру + регистрирует в родителе (Configuration.xml или родительская подсистема).
|
||||
|
||||
## Параметры и команда
|
||||
|
||||
| Параметр | Описание |
|
||||
|----------|----------|
|
||||
| `DefinitionFile` | Путь к JSON-файлу определения |
|
||||
| `Value` | Инлайн JSON-строка (альтернатива DefinitionFile) |
|
||||
| `OutputDir` | Корень выгрузки (где `Subsystems/`, `Configuration.xml`) |
|
||||
| `Parent` | Путь к XML родительской подсистемы (для вложенных) |
|
||||
| `NoValidate` | Пропустить авто-валидацию |
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File '.claude\skills\subsystem-compile\scripts\subsystem-compile.ps1' -Value '<json>' -OutputDir '<ConfigDir>'
|
||||
```
|
||||
|
||||
## JSON-определение
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "МояПодсистема",
|
||||
"synonym": "Моя подсистема",
|
||||
"comment": "",
|
||||
"includeInCommandInterface": true,
|
||||
"useOneCommand": false,
|
||||
"explanation": "Описание раздела",
|
||||
"picture": "CommonPicture.МояКартинка",
|
||||
"content": ["Catalog.Товары", "Document.Заказ"],
|
||||
"children": ["ДочерняяА", "ДочерняяБ"]
|
||||
}
|
||||
```
|
||||
|
||||
Минимально: только `name`. Остальное — дефолты.
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Минимальная подсистема
|
||||
... -Value '{"name":"Тест"}' -OutputDir config/
|
||||
|
||||
# С составом и картинкой
|
||||
... -Value '{"name":"Продажи","content":["Catalog.Товары","Report.Продажи"],"picture":"CommonPicture.Продажи"}' -OutputDir config/
|
||||
|
||||
# Вложенная подсистема
|
||||
... -Value '{"name":"Дочерняя"}' -OutputDir config/ -Parent config/Subsystems/Продажи.xml
|
||||
```
|
||||
|
||||
## Что генерируется
|
||||
|
||||
- `{OutputDir}/Subsystems/{Name}.xml` — определение подсистемы
|
||||
- `{OutputDir}/Subsystems/{Name}/` — каталог (если есть children)
|
||||
- `Configuration.xml` или родительская подсистема — регистрация в `<ChildObjects>`
|
||||
@@ -0,0 +1,338 @@
|
||||
# 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
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
name: subsystem-edit
|
||||
description: Точечное редактирование подсистемы 1С — добавление/удаление объектов в Content, дочерних подсистем, изменение свойств
|
||||
argument-hint: -SubsystemPath <path> -Operation <op> -Value <value>
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Write
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /subsystem-edit — редактирование подсистемы 1С
|
||||
|
||||
Точечное редактирование XML подсистемы: состав, дочерние подсистемы, свойства.
|
||||
|
||||
## Параметры и команда
|
||||
|
||||
| Параметр | Описание |
|
||||
|----------|----------|
|
||||
| `SubsystemPath` | Путь к XML-файлу подсистемы |
|
||||
| `DefinitionFile` | JSON-файл с массивом операций |
|
||||
| `Operation` | Одна операция (альтернатива DefinitionFile) |
|
||||
| `Value` | Значение для операции |
|
||||
| `NoValidate` | Пропустить авто-валидацию |
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File '.claude\skills\subsystem-edit\scripts\subsystem-edit.ps1' -SubsystemPath '<path>' -Operation add-content -Value 'Catalog.Товары'
|
||||
```
|
||||
|
||||
## Операции
|
||||
|
||||
| Операция | Значение | Описание |
|
||||
|----------|----------|----------|
|
||||
| `add-content` | `"Catalog.X"` или `["Catalog.X","Document.Y"]` | Добавить объекты в Content |
|
||||
| `remove-content` | `"Catalog.X"` или `["Catalog.X"]` | Удалить объекты из Content |
|
||||
| `add-child` | `"ИмяПодсистемы"` | Добавить дочернюю подсистему в ChildObjects |
|
||||
| `remove-child` | `"ИмяПодсистемы"` | Удалить дочернюю подсистему |
|
||||
| `set-property` | `{"name":"prop","value":"val"}` | Изменить свойство (Synonym, IncludeInCommandInterface, UseOneCommand, etc.) |
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Добавить объект в состав
|
||||
... -SubsystemPath Subsystems/Продажи.xml -Operation add-content -Value "Document.Заказ"
|
||||
|
||||
# Добавить несколько объектов
|
||||
... -SubsystemPath Subsystems/Продажи.xml -Operation add-content -Value '["Catalog.Товары","Report.Продажи"]'
|
||||
|
||||
# Удалить объект из состава
|
||||
... -SubsystemPath Subsystems/Продажи.xml -Operation remove-content -Value "Report.Старый"
|
||||
|
||||
# Добавить дочернюю подсистему
|
||||
... -SubsystemPath Subsystems/Продажи.xml -Operation add-child -Value "НоваяДочерняя"
|
||||
|
||||
# Изменить свойство
|
||||
... -SubsystemPath Subsystems/Продажи.xml -Operation set-property -Value '{"name":"IncludeInCommandInterface","value":"false"}'
|
||||
```
|
||||
@@ -0,0 +1,403 @@
|
||||
# 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"
|
||||
if (Test-Path $candidate) { $SubsystemPath = $candidate }
|
||||
else { Write-Error "No $dirName.xml found in directory"; exit 1 }
|
||||
}
|
||||
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
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
name: subsystem-info
|
||||
description: Анализ структуры подсистемы 1С из XML-выгрузки — состав, дочерние подсистемы, командный интерфейс, дерево иерархии. Используй для изучения структуры подсистем и навигации по конфигурации
|
||||
argument-hint: <SubsystemPath> [-Mode overview|content|ci|tree] [-Name <элемент>]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /subsystem-info — Структура подсистемы 1С
|
||||
|
||||
Читает XML подсистемы из выгрузки конфигурации 1С и выводит компактное описание структуры.
|
||||
|
||||
## Параметры и команда
|
||||
|
||||
| Параметр | Описание |
|
||||
|----------|----------|
|
||||
| `SubsystemPath` | Путь к XML-файлу подсистемы, каталогу подсистемы или каталогу `Subsystems/` (для tree) |
|
||||
| `Mode` | Режим: `overview` (default), `content`, `ci`, `tree` |
|
||||
| `Name` | Drill-down: тип объекта в content, секция в ci, имя подсистемы в tree |
|
||||
| `Limit` / `Offset` | Пагинация (по умолчанию 150 строк) |
|
||||
| `OutFile` | Записать результат в файл (UTF-8 BOM) |
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude\skills\subsystem-info\scripts\subsystem-info.ps1 -SubsystemPath "<путь>"
|
||||
```
|
||||
|
||||
## Четыре режима
|
||||
|
||||
| Режим | Что показывает |
|
||||
|---|---|
|
||||
| `overview` *(default)* | Компактная сводка: свойства, состав (сгруппирован по типам), дочерние подсистемы, наличие CI |
|
||||
| `content` | Список Content с группировкой по типу объекта. `-Name Catalog` — только каталоги |
|
||||
| `ci` | Разбор CommandInterface.xml: видимость, размещение, порядок команд/подсистем/групп |
|
||||
| `tree` | Рекурсивное дерево иерархии подсистем с маркерами [CI], [OneCmd], [Скрыт] |
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Обзор подсистемы
|
||||
... -SubsystemPath Subsystems/Продажи.xml
|
||||
|
||||
# Состав подсистемы
|
||||
... -SubsystemPath Subsystems/Администрирование.xml -Mode content
|
||||
|
||||
# Только документы в составе
|
||||
... -SubsystemPath Subsystems/Продажи.xml -Mode content -Name Document
|
||||
|
||||
# Командный интерфейс подсистемы
|
||||
... -SubsystemPath Subsystems/Продажи.xml -Mode ci
|
||||
|
||||
# Дерево подсистем от корня
|
||||
... -SubsystemPath Subsystems -Mode tree
|
||||
|
||||
# Дерево от конкретной подсистемы
|
||||
... -SubsystemPath Subsystems/Администрирование.xml -Mode tree
|
||||
|
||||
# Дерево только для одной подсистемы
|
||||
... -SubsystemPath Subsystems -Mode tree -Name Администрирование
|
||||
```
|
||||
@@ -0,0 +1,483 @@
|
||||
# subsystem-info v1.0 — Compact summary of 1C subsystem structure
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$SubsystemPath,
|
||||
[ValidateSet("overview","content","ci","tree")]
|
||||
[string]$Mode = "overview",
|
||||
[string]$Name,
|
||||
[int]$Limit = 150,
|
||||
[int]$Offset = 0,
|
||||
[string]$OutFile
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Output helper ---
|
||||
$script:lines = @()
|
||||
function Out([string]$text) { $script:lines += $text }
|
||||
|
||||
# --- Resolve path ---
|
||||
if (-not [System.IO.Path]::IsPathRooted($SubsystemPath)) {
|
||||
$SubsystemPath = Join-Path (Get-Location).Path $SubsystemPath
|
||||
}
|
||||
|
||||
# --- Helper: get LocalString text ---
|
||||
function Get-MLText($node) {
|
||||
if (-not $node -or -not $node.HasChildNodes) { return "" }
|
||||
foreach ($item in $node.ChildNodes) {
|
||||
if ($item.NodeType -ne 'Element') { continue }
|
||||
$lang = ""; $content = ""
|
||||
foreach ($c in $item.ChildNodes) {
|
||||
if ($c.NodeType -ne 'Element') { continue }
|
||||
if ($c.LocalName -eq "lang") { $lang = $c.InnerText }
|
||||
if ($c.LocalName -eq "content") { $content = $c.InnerText }
|
||||
}
|
||||
if ($lang -eq "ru" -and $content) { return $content }
|
||||
}
|
||||
# fallback: first item
|
||||
foreach ($item in $node.ChildNodes) {
|
||||
if ($item.NodeType -ne 'Element') { continue }
|
||||
foreach ($c in $item.ChildNodes) {
|
||||
if ($c.NodeType -ne 'Element') { continue }
|
||||
if ($c.LocalName -eq "content" -and $c.InnerText) { return $c.InnerText }
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
# --- Helper: load subsystem XML ---
|
||||
function Load-SubsystemXml([string]$xmlPath) {
|
||||
[xml]$doc = Get-Content -Path $xmlPath -Encoding UTF8
|
||||
$ns = New-Object System.Xml.XmlNamespaceManager($doc.NameTable)
|
||||
$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||
$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core")
|
||||
$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable")
|
||||
$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance")
|
||||
$sub = $doc.SelectSingleNode("/md:MetaDataObject/md:Subsystem", $ns)
|
||||
if (-not $sub) {
|
||||
Write-Host "[ERROR] Not a valid subsystem XML: $xmlPath"
|
||||
exit 1
|
||||
}
|
||||
return @{ Doc=$doc; Ns=$ns; Sub=$sub }
|
||||
}
|
||||
|
||||
# --- Helper: get content items ---
|
||||
function Get-ContentItems($props, $ns) {
|
||||
$items = @()
|
||||
$contentNode = $props.SelectSingleNode("md:Content", $ns)
|
||||
if (-not $contentNode -or -not $contentNode.HasChildNodes) { return $items }
|
||||
foreach ($item in $contentNode.SelectNodes("xr:Item", $ns)) {
|
||||
$items += $item.InnerText
|
||||
}
|
||||
return $items
|
||||
}
|
||||
|
||||
# --- Helper: get child subsystem names ---
|
||||
function Get-ChildNames($sub, $ns) {
|
||||
$names = @()
|
||||
$co = $sub.SelectSingleNode("md:ChildObjects", $ns)
|
||||
if (-not $co -or -not $co.HasChildNodes) { return $names }
|
||||
foreach ($child in $co.ChildNodes) {
|
||||
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Subsystem") {
|
||||
$names += $child.InnerText
|
||||
}
|
||||
}
|
||||
return $names
|
||||
}
|
||||
|
||||
# --- Helper: group content by type ---
|
||||
function Group-ContentByType($items) {
|
||||
$groups = [ordered]@{}
|
||||
foreach ($item in $items) {
|
||||
if ($item -match '^([^.]+)\.(.+)$') {
|
||||
$type = $Matches[1]
|
||||
$name = $Matches[2]
|
||||
} elseif ($item -match '^[0-9a-fA-F]{8}-') {
|
||||
$type = "[UUID]"
|
||||
$name = $item
|
||||
} else {
|
||||
$type = "[Other]"
|
||||
$name = $item
|
||||
}
|
||||
if (-not $groups.Contains($type)) { $groups[$type] = @() }
|
||||
$groups[$type] += $name
|
||||
}
|
||||
return $groups
|
||||
}
|
||||
|
||||
# --- Helper: find subsystem dir from XML path ---
|
||||
function Get-SubsystemDir([string]$xmlPath) {
|
||||
$dir = [System.IO.Path]::GetDirectoryName($xmlPath)
|
||||
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($xmlPath)
|
||||
return Join-Path $dir $baseName
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# Mode: tree
|
||||
# ============================================================
|
||||
if ($Mode -eq "tree") {
|
||||
$isDir = Test-Path $SubsystemPath -PathType Container
|
||||
$rootDir = $null
|
||||
$rootXml = $null
|
||||
|
||||
if ($isDir) {
|
||||
# Subsystems/ directory — show all top-level subsystems
|
||||
$rootDir = $SubsystemPath
|
||||
} else {
|
||||
# Specific subsystem XML — show tree from this subsystem
|
||||
if (-not (Test-Path $SubsystemPath)) {
|
||||
Write-Host "[ERROR] File not found: $SubsystemPath"
|
||||
exit 1
|
||||
}
|
||||
$rootXml = $SubsystemPath
|
||||
}
|
||||
|
||||
# Box-drawing chars (PS 5.1 compatible)
|
||||
$script:T_BRANCH = [char]0x251C + [char]0x2500 + [char]0x2500 + " " # ├──
|
||||
$script:T_LAST = [char]0x2514 + [char]0x2500 + [char]0x2500 + " " # └──
|
||||
$script:T_PIPE = [char]0x2502 + " " # │
|
||||
$script:T_ARROW = [char]0x2192 # →
|
||||
|
||||
function Get-TreeLine([string]$xmlPath) {
|
||||
$parsed = Load-SubsystemXml $xmlPath
|
||||
$sub = $parsed.Sub; $ns = $parsed.Ns
|
||||
$props = $sub.SelectSingleNode("md:Properties", $ns)
|
||||
$name = $props.SelectSingleNode("md:Name", $ns).InnerText
|
||||
|
||||
$markers = @()
|
||||
$subDir = Get-SubsystemDir $xmlPath
|
||||
$ciPath = Join-Path (Join-Path $subDir "Ext") "CommandInterface.xml"
|
||||
if (Test-Path $ciPath) { $markers += "CI" }
|
||||
$useOne = $props.SelectSingleNode("md:UseOneCommand", $ns)
|
||||
if ($useOne -and $useOne.InnerText -eq "true") { $markers += "OneCmd" }
|
||||
$inclCI = $props.SelectSingleNode("md:IncludeInCommandInterface", $ns)
|
||||
if ($inclCI -and $inclCI.InnerText -eq "false") { $markers += "Скрыт" }
|
||||
$markerStr = if ($markers.Count -gt 0) { " [$($markers -join ', ')]" } else { "" }
|
||||
|
||||
$contentItems = @(Get-ContentItems $props $ns)
|
||||
$childNames = @(Get-ChildNames $sub $ns)
|
||||
$childStr = if ($childNames.Count -gt 0) { ", $($childNames.Count) дочерних" } else { "" }
|
||||
|
||||
return @{
|
||||
Label = "$name$markerStr ($($contentItems.Count) объектов$childStr)"
|
||||
SubDir = $subDir
|
||||
ChildNames = $childNames
|
||||
}
|
||||
}
|
||||
|
||||
function Build-TreeEntry([string]$xmlPath, [string]$prefix, [bool]$isLast, [bool]$isRoot) {
|
||||
$info = Get-TreeLine $xmlPath
|
||||
|
||||
$connector = if ($isRoot) { "" } elseif ($isLast) { $script:T_LAST } else { $script:T_BRANCH }
|
||||
Out "$prefix$connector$($info.Label)"
|
||||
|
||||
if ($info.ChildNames.Count -gt 0) {
|
||||
$childPrefix = if ($isRoot) { "" } elseif ($isLast) { "$prefix " } else { "$prefix$($script:T_PIPE)" }
|
||||
$subsDir = Join-Path $info.SubDir "Subsystems"
|
||||
for ($i = 0; $i -lt $info.ChildNames.Count; $i++) {
|
||||
$childXml = Join-Path $subsDir "$($info.ChildNames[$i]).xml"
|
||||
$childIsLast = ($i -eq $info.ChildNames.Count - 1)
|
||||
if (Test-Path $childXml) {
|
||||
Build-TreeEntry $childXml $childPrefix $childIsLast $false
|
||||
} else {
|
||||
$conn2 = if ($childIsLast) { $script:T_LAST } else { $script:T_BRANCH }
|
||||
Out "$childPrefix$conn2$($info.ChildNames[$i]) [NOT FOUND]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($rootDir) {
|
||||
$label = Split-Path $rootDir -Leaf
|
||||
Out "Дерево подсистем от: $label/"
|
||||
Out ""
|
||||
$xmlFiles = @(Get-ChildItem $rootDir -Filter "*.xml" -File | Sort-Object Name)
|
||||
if ($Name) {
|
||||
$xmlFiles = @($xmlFiles | Where-Object { $_.BaseName -eq $Name })
|
||||
if ($xmlFiles.Count -eq 0) {
|
||||
Write-Host "[ERROR] Subsystem '$Name' not found in $rootDir"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
for ($i = 0; $i -lt $xmlFiles.Count; $i++) {
|
||||
Build-TreeEntry $xmlFiles[$i].FullName "" ($i -eq $xmlFiles.Count - 1) $true
|
||||
}
|
||||
} else {
|
||||
Build-TreeEntry $rootXml "" $true $true
|
||||
}
|
||||
|
||||
} elseif ($Mode -eq "ci") {
|
||||
# ============================================================
|
||||
# Mode: ci — CommandInterface.xml
|
||||
# ============================================================
|
||||
if (Test-Path $SubsystemPath -PathType Container) {
|
||||
Write-Host "[ERROR] ci mode requires a subsystem .xml file, not a directory"
|
||||
exit 1
|
||||
}
|
||||
if (-not (Test-Path $SubsystemPath)) {
|
||||
Write-Host "[ERROR] File not found: $SubsystemPath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$parsed = Load-SubsystemXml $SubsystemPath
|
||||
$sub = $parsed.Sub; $ns = $parsed.Ns
|
||||
$props = $sub.SelectSingleNode("md:Properties", $ns)
|
||||
$subName = $props.SelectSingleNode("md:Name", $ns).InnerText
|
||||
|
||||
$subDir = Get-SubsystemDir $SubsystemPath
|
||||
$ciPath = Join-Path (Join-Path $subDir "Ext") "CommandInterface.xml"
|
||||
|
||||
if (-not (Test-Path $ciPath)) {
|
||||
Out "Командный интерфейс: $subName"
|
||||
Out ""
|
||||
Out "Файл CommandInterface.xml не найден."
|
||||
Out "Путь: $ciPath"
|
||||
} else {
|
||||
[xml]$ciDoc = Get-Content -Path $ciPath -Encoding UTF8
|
||||
$ciNs = New-Object System.Xml.XmlNamespaceManager($ciDoc.NameTable)
|
||||
$ciNs.AddNamespace("ci", "http://v8.1c.ru/8.3/xcf/extrnprops")
|
||||
$ciNs.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable")
|
||||
|
||||
$ciRoot = $ciDoc.DocumentElement
|
||||
|
||||
Out "Командный интерфейс: $subName"
|
||||
Out ""
|
||||
|
||||
# --- CommandsVisibility ---
|
||||
$visSection = $ciRoot.SelectSingleNode("ci:CommandsVisibility", $ciNs)
|
||||
if ($visSection) {
|
||||
$hidden = @(); $shown = @()
|
||||
foreach ($cmd in $visSection.SelectNodes("ci:Command", $ciNs)) {
|
||||
$cmdName = $cmd.GetAttribute("name")
|
||||
$vis = $cmd.SelectSingleNode("ci:Visibility/xr:Common", $ciNs)
|
||||
if ($vis -and $vis.InnerText -eq "false") { $hidden += $cmdName }
|
||||
else { $shown += $cmdName }
|
||||
}
|
||||
$total = $hidden.Count + $shown.Count
|
||||
if (-not $Name -or $Name -eq "visibility") {
|
||||
Out "Видимость ($total):"
|
||||
if ($hidden.Count -gt 0) {
|
||||
Out " СКРЫТО ($($hidden.Count)):"
|
||||
foreach ($h in $hidden) { Out " $h" }
|
||||
}
|
||||
if ($shown.Count -gt 0) {
|
||||
Out " ПОКАЗАНО ($($shown.Count)):"
|
||||
foreach ($s in $shown) { Out " $s" }
|
||||
}
|
||||
Out ""
|
||||
}
|
||||
}
|
||||
|
||||
# --- CommandsPlacement ---
|
||||
$placeSection = $ciRoot.SelectSingleNode("ci:CommandsPlacement", $ciNs)
|
||||
if ($placeSection) {
|
||||
$placements = @()
|
||||
foreach ($cmd in $placeSection.SelectNodes("ci:Command", $ciNs)) {
|
||||
$cmdName = $cmd.GetAttribute("name")
|
||||
$grp = $cmd.SelectSingleNode("ci:CommandGroup", $ciNs)
|
||||
$pl = $cmd.SelectSingleNode("ci:Placement", $ciNs)
|
||||
$grpText = if ($grp) { $grp.InnerText } else { "?" }
|
||||
$plText = if ($pl) { $pl.InnerText } else { "?" }
|
||||
$placements += @{ Name=$cmdName; Group=$grpText; Placement=$plText }
|
||||
}
|
||||
if ((-not $Name -or $Name -eq "placement") -and $placements.Count -gt 0) {
|
||||
Out "Размещение ($($placements.Count)):"
|
||||
$arrow = [char]0x2192
|
||||
foreach ($p in $placements) {
|
||||
Out " $($p.Name) $arrow $($p.Group) ($($p.Placement))"
|
||||
}
|
||||
Out ""
|
||||
}
|
||||
}
|
||||
|
||||
# --- CommandsOrder ---
|
||||
$orderSection = $ciRoot.SelectSingleNode("ci:CommandsOrder", $ciNs)
|
||||
if ($orderSection) {
|
||||
$orderGroups = [ordered]@{}
|
||||
foreach ($cmd in $orderSection.SelectNodes("ci:Command", $ciNs)) {
|
||||
$cmdName = $cmd.GetAttribute("name")
|
||||
$grp = $cmd.SelectSingleNode("ci:CommandGroup", $ciNs)
|
||||
$grpText = if ($grp) { $grp.InnerText } else { "?" }
|
||||
if (-not $orderGroups.Contains($grpText)) { $orderGroups[$grpText] = @() }
|
||||
$orderGroups[$grpText] += $cmdName
|
||||
}
|
||||
$totalOrder = 0
|
||||
foreach ($k in $orderGroups.Keys) { $totalOrder += $orderGroups[$k].Count }
|
||||
if ((-not $Name -or $Name -eq "order") -and $totalOrder -gt 0) {
|
||||
Out "Порядок команд ($totalOrder):"
|
||||
foreach ($grpName in $orderGroups.Keys) {
|
||||
Out " [$grpName]:"
|
||||
foreach ($c in $orderGroups[$grpName]) { Out " $c" }
|
||||
}
|
||||
Out ""
|
||||
}
|
||||
}
|
||||
|
||||
# --- SubsystemsOrder ---
|
||||
$subOrderSection = $ciRoot.SelectSingleNode("ci:SubsystemsOrder", $ciNs)
|
||||
if ($subOrderSection) {
|
||||
$subOrder = @()
|
||||
foreach ($s in $subOrderSection.SelectNodes("ci:Subsystem", $ciNs)) {
|
||||
$subOrder += $s.InnerText
|
||||
}
|
||||
if ((-not $Name -or $Name -eq "subsystems") -and $subOrder.Count -gt 0) {
|
||||
Out "Порядок подсистем ($($subOrder.Count)):"
|
||||
for ($i = 0; $i -lt $subOrder.Count; $i++) {
|
||||
Out " $($i+1). $($subOrder[$i])"
|
||||
}
|
||||
Out ""
|
||||
}
|
||||
}
|
||||
|
||||
# --- GroupsOrder ---
|
||||
$grpOrderSection = $ciRoot.SelectSingleNode("ci:GroupsOrder", $ciNs)
|
||||
if ($grpOrderSection) {
|
||||
$grpOrder = @()
|
||||
foreach ($g in $grpOrderSection.SelectNodes("ci:Group", $ciNs)) {
|
||||
$grpOrder += $g.InnerText
|
||||
}
|
||||
if ((-not $Name -or $Name -eq "groups") -and $grpOrder.Count -gt 0) {
|
||||
Out "Порядок групп ($($grpOrder.Count)):"
|
||||
foreach ($g in $grpOrder) { Out " $g" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
# ============================================================
|
||||
# Mode: overview / content — requires a subsystem XML file
|
||||
# ============================================================
|
||||
if (Test-Path $SubsystemPath -PathType Container) {
|
||||
$dirName = Split-Path $SubsystemPath -Leaf
|
||||
$candidate = Join-Path $SubsystemPath "$dirName.xml"
|
||||
if (Test-Path $candidate) {
|
||||
$SubsystemPath = $candidate
|
||||
} else {
|
||||
Write-Host "[ERROR] No $dirName.xml found in directory. Use -Mode tree for directory listing."
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
if (-not (Test-Path $SubsystemPath)) {
|
||||
Write-Host "[ERROR] File not found: $SubsystemPath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$parsed = Load-SubsystemXml $SubsystemPath
|
||||
$sub = $parsed.Sub; $ns = $parsed.Ns
|
||||
$props = $sub.SelectSingleNode("md:Properties", $ns)
|
||||
|
||||
$subName = $props.SelectSingleNode("md:Name", $ns).InnerText
|
||||
$synonym = Get-MLText $props.SelectSingleNode("md:Synonym", $ns)
|
||||
$comment = $props.SelectSingleNode("md:Comment", $ns)
|
||||
$commentText = if ($comment -and $comment.InnerText) { $comment.InnerText } else { "" }
|
||||
$inclHelp = $props.SelectSingleNode("md:IncludeHelpInContents", $ns).InnerText
|
||||
$inclCI = $props.SelectSingleNode("md:IncludeInCommandInterface", $ns).InnerText
|
||||
$useOneCmd = $props.SelectSingleNode("md:UseOneCommand", $ns).InnerText
|
||||
$explanation = Get-MLText $props.SelectSingleNode("md:Explanation", $ns)
|
||||
|
||||
# Picture
|
||||
$picNode = $props.SelectSingleNode("md:Picture", $ns)
|
||||
$picText = ""
|
||||
if ($picNode -and $picNode.HasChildNodes) {
|
||||
$picRef = $picNode.SelectSingleNode("xr:Ref", $ns)
|
||||
if ($picRef -and $picRef.InnerText) { $picText = $picRef.InnerText }
|
||||
}
|
||||
|
||||
# Content
|
||||
$contentItems = @(Get-ContentItems $props $ns)
|
||||
$groups = Group-ContentByType $contentItems
|
||||
|
||||
# Children
|
||||
$childNames = @(Get-ChildNames $sub $ns)
|
||||
|
||||
# CI presence
|
||||
$subDir = Get-SubsystemDir $SubsystemPath
|
||||
$ciPath = Join-Path (Join-Path $subDir "Ext") "CommandInterface.xml"
|
||||
$hasCI = Test-Path $ciPath
|
||||
|
||||
if ($Mode -eq "overview") {
|
||||
Out "Подсистема: $subName"
|
||||
if ($synonym -and $synonym -ne $subName) { Out "Синоним: $synonym" }
|
||||
if ($commentText) { Out "Комментарий: $commentText" }
|
||||
Out "ВключатьВКомандныйИнтерфейс: $inclCI"
|
||||
Out "ИспользоватьОднуКоманду: $useOneCmd"
|
||||
if ($explanation) { Out "Пояснение: $explanation" }
|
||||
if ($picText) { Out "Картинка: $picText" }
|
||||
|
||||
# Content summary
|
||||
if ($contentItems.Count -gt 0) {
|
||||
$parts = @()
|
||||
foreach ($type in $groups.Keys) {
|
||||
$parts += "$type`: $($groups[$type].Count)"
|
||||
}
|
||||
Out "Состав: $($contentItems.Count) объектов ($($parts -join ', '))"
|
||||
} else {
|
||||
Out "Состав: пусто"
|
||||
}
|
||||
|
||||
# Children
|
||||
if ($childNames.Count -gt 0) {
|
||||
Out "Дочерние подсистемы ($($childNames.Count)): $($childNames -join ', ')"
|
||||
}
|
||||
|
||||
# CI
|
||||
if ($hasCI) {
|
||||
Out "Командный интерфейс: есть"
|
||||
}
|
||||
|
||||
} elseif ($Mode -eq "content") {
|
||||
Out "Состав подсистемы $subName ($($contentItems.Count) объектов):"
|
||||
Out ""
|
||||
|
||||
if ($Name) {
|
||||
# Filter by type
|
||||
if ($groups.Contains($Name)) {
|
||||
$filtered = $groups[$Name]
|
||||
Out "$Name ($($filtered.Count)):"
|
||||
foreach ($n in $filtered) { Out " $n" }
|
||||
} else {
|
||||
Out "[INFO] Тип '$Name' не найден в составе."
|
||||
Out "Доступные типы: $($groups.Keys -join ', ')"
|
||||
}
|
||||
} else {
|
||||
foreach ($type in $groups.Keys) {
|
||||
Out "$type ($($groups[$type].Count)):"
|
||||
foreach ($n in $groups[$type]) { Out " $n" }
|
||||
Out ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Pagination and output ---
|
||||
$totalLines = $script:lines.Count
|
||||
$outLines = $script:lines
|
||||
|
||||
if ($Offset -gt 0) {
|
||||
if ($Offset -ge $totalLines) {
|
||||
Write-Host "[INFO] Offset $Offset exceeds total lines ($totalLines). Nothing to show."
|
||||
exit 0
|
||||
}
|
||||
$outLines = $outLines[$Offset..($totalLines - 1)]
|
||||
}
|
||||
|
||||
if ($Limit -gt 0 -and $outLines.Count -gt $Limit) {
|
||||
$shown = $outLines[0..($Limit - 1)]
|
||||
$remaining = $totalLines - $Offset - $Limit
|
||||
$shown += ""
|
||||
$shown += "[ОБРЕЗАНО] Показано $Limit из $totalLines строк. Используйте -Offset $($Offset + $Limit) для продолжения."
|
||||
$outLines = $shown
|
||||
}
|
||||
|
||||
if ($OutFile) {
|
||||
if (-not [System.IO.Path]::IsPathRooted($OutFile)) {
|
||||
$OutFile = Join-Path (Get-Location).Path $OutFile
|
||||
}
|
||||
$utf8 = New-Object System.Text.UTF8Encoding($true)
|
||||
[System.IO.File]::WriteAllLines($OutFile, $outLines, $utf8)
|
||||
Write-Host "Output written to $OutFile"
|
||||
} else {
|
||||
foreach ($l in $outLines) { Write-Host $l }
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
name: subsystem-validate
|
||||
description: Валидация структурной корректности подсистемы 1С — XML-формат, свойства, состав, дочерние подсистемы, командный интерфейс
|
||||
argument-hint: <SubsystemPath> [-MaxErrors 30]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /subsystem-validate — валидация подсистемы 1С
|
||||
|
||||
Проверяет структурную корректность XML-файла подсистемы из выгрузки конфигурации.
|
||||
|
||||
## Параметры и команда
|
||||
|
||||
| Параметр | Описание |
|
||||
|----------|----------|
|
||||
| `SubsystemPath` | Путь к XML-файлу подсистемы |
|
||||
| `MaxErrors` | Максимум ошибок до остановки (по умолчанию 30) |
|
||||
| `OutFile` | Записать результат в файл |
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File '.claude\skills\subsystem-validate\scripts\subsystem-validate.ps1' -SubsystemPath '<путь>'
|
||||
```
|
||||
|
||||
## Проверки (13)
|
||||
|
||||
1. XML well-formedness + root structure (MetaDataObject/Subsystem)
|
||||
2. Properties — 9 обязательных свойств
|
||||
3. Name — непустой, валидный идентификатор
|
||||
4. Synonym — непустой (хотя бы один v8:item)
|
||||
5. Булевы свойства — содержат true/false
|
||||
6. Content — формат xr:Item, xsi:type
|
||||
7. Content — нет дубликатов
|
||||
8. ChildObjects — элементы непустые
|
||||
9. ChildObjects — нет дубликатов
|
||||
10. ChildObjects → файлы существуют
|
||||
11. CommandInterface.xml — well-formedness
|
||||
12. Picture — формат ссылки
|
||||
13. UseOneCommand=true → ровно 1 элемент в Content
|
||||
@@ -0,0 +1,314 @@
|
||||
# subsystem-validate v1.0 — Validate 1C subsystem XML structure
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$SubsystemPath,
|
||||
[int]$MaxErrors = 30,
|
||||
[string]$OutFile
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- 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"
|
||||
if (Test-Path $candidate) { $SubsystemPath = $candidate }
|
||||
else {
|
||||
Write-Host "[ERROR] No $dirName.xml found in directory: $SubsystemPath"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
if (-not (Test-Path $SubsystemPath)) {
|
||||
Write-Host "[ERROR] File not found: $SubsystemPath"
|
||||
exit 1
|
||||
}
|
||||
$resolvedPath = (Resolve-Path $SubsystemPath).Path
|
||||
|
||||
# --- Output infrastructure ---
|
||||
$script:errors = 0
|
||||
$script:warnings = 0
|
||||
$script:stopped = $false
|
||||
$script:output = New-Object System.Text.StringBuilder 8192
|
||||
|
||||
function Out-Line([string]$msg) { $script:output.AppendLine($msg) | Out-Null }
|
||||
function Report-OK([string]$msg) { Out-Line "[OK] $msg" }
|
||||
function Report-Error([string]$msg) {
|
||||
$script:errors++
|
||||
Out-Line "[ERROR] $msg"
|
||||
if ($script:errors -ge $MaxErrors) { $script:stopped = $true }
|
||||
}
|
||||
function Report-Warn([string]$msg) {
|
||||
$script:warnings++
|
||||
Out-Line "[WARN] $msg"
|
||||
}
|
||||
|
||||
$guidPattern = '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
|
||||
$identPattern = '^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_][A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$'
|
||||
|
||||
# --- 1. XML well-formedness + root structure ---
|
||||
$xmlDoc = $null
|
||||
try {
|
||||
[xml]$xmlDoc = Get-Content -Path $resolvedPath -Encoding UTF8
|
||||
} catch {
|
||||
Report-Error "1. XML parse error: $($_.Exception.Message)"
|
||||
$script:stopped = $true
|
||||
}
|
||||
|
||||
if (-not $script:stopped) {
|
||||
$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
|
||||
$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||
$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core")
|
||||
$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable")
|
||||
$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance")
|
||||
|
||||
$root = $xmlDoc.DocumentElement
|
||||
$sub = $xmlDoc.SelectSingleNode("/md:MetaDataObject/md:Subsystem", $ns)
|
||||
$version = $root.GetAttribute("version")
|
||||
|
||||
if (-not $sub) {
|
||||
Report-Error "1. Root structure: expected MetaDataObject/Subsystem, not found"
|
||||
$script:stopped = $true
|
||||
} else {
|
||||
$uuid = $sub.GetAttribute("uuid")
|
||||
if ($uuid -and $uuid -match $guidPattern) {
|
||||
Report-OK "1. Root structure: MetaDataObject/Subsystem, uuid=$uuid, version $version"
|
||||
} else {
|
||||
Report-Error "1. Root structure: invalid or missing uuid"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Properties checks ---
|
||||
if (-not $script:stopped) {
|
||||
$props = $sub.SelectSingleNode("md:Properties", $ns)
|
||||
if (-not $props) {
|
||||
Report-Error "2. Properties: <Properties> element not found"
|
||||
$script:stopped = $true
|
||||
}
|
||||
}
|
||||
|
||||
$subName = ""
|
||||
if (-not $script:stopped) {
|
||||
# --- 2. Required properties ---
|
||||
$requiredProps = @("Name","Synonym","Comment","IncludeHelpInContents","IncludeInCommandInterface","UseOneCommand","Explanation","Picture","Content")
|
||||
$missing = @()
|
||||
foreach ($p in $requiredProps) {
|
||||
$el = $props.SelectSingleNode("md:$p", $ns)
|
||||
if (-not $el) { $missing += $p }
|
||||
}
|
||||
if ($missing.Count -eq 0) {
|
||||
Report-OK "2. Properties: all 9 required properties present"
|
||||
} else {
|
||||
Report-Error "2. Properties: missing: $($missing -join ', ')"
|
||||
}
|
||||
|
||||
# --- 3. Name ---
|
||||
$nameEl = $props.SelectSingleNode("md:Name", $ns)
|
||||
$subName = if ($nameEl) { $nameEl.InnerText.Trim() } else { "" }
|
||||
Out-Line ""
|
||||
Out-Line "=== Validation: Subsystem.$subName ==="
|
||||
# Re-insert header at position 0
|
||||
$headerLine = "=== Validation: Subsystem.$subName ==="
|
||||
$script:output.Insert(0, "$headerLine`r`n`r`n") | Out-Null
|
||||
|
||||
if ($subName -and $subName -match $identPattern) {
|
||||
Report-OK "3. Name: `"$subName`" - valid identifier"
|
||||
} elseif (-not $subName) {
|
||||
Report-Error "3. Name: empty"
|
||||
} else {
|
||||
Report-Error "3. Name: `"$subName`" - invalid identifier"
|
||||
}
|
||||
|
||||
# --- 4. Synonym ---
|
||||
$synEl = $props.SelectSingleNode("md:Synonym", $ns)
|
||||
if ($synEl -and $synEl.HasChildNodes) {
|
||||
$items = $synEl.SelectNodes("v8:item", $ns)
|
||||
if ($items.Count -gt 0) {
|
||||
$firstContent = ""
|
||||
foreach ($item in $items) {
|
||||
$c = $item.SelectSingleNode("v8:content", $ns)
|
||||
if ($c -and $c.InnerText) { $firstContent = $c.InnerText; break }
|
||||
}
|
||||
Report-OK "4. Synonym: `"$firstContent`" ($($items.Count) lang(s))"
|
||||
} else {
|
||||
Report-Warn "4. Synonym: element exists but no v8:item children"
|
||||
}
|
||||
} else {
|
||||
Report-Warn "4. Synonym: empty or missing"
|
||||
}
|
||||
|
||||
# --- 5. Boolean properties ---
|
||||
$boolProps = @("IncludeHelpInContents","IncludeInCommandInterface","UseOneCommand")
|
||||
$boolOk = $true
|
||||
$boolVals = @{}
|
||||
foreach ($bp in $boolProps) {
|
||||
$el = $props.SelectSingleNode("md:$bp", $ns)
|
||||
if ($el) {
|
||||
$val = $el.InnerText.Trim()
|
||||
$boolVals[$bp] = $val
|
||||
if ($val -ne "true" -and $val -ne "false") {
|
||||
Report-Error "5. Boolean property $bp = `"$val`" (expected true/false)"
|
||||
$boolOk = $false
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($boolOk) { Report-OK "5. Boolean properties: valid" }
|
||||
|
||||
# --- 6. Content items format ---
|
||||
$contentEl = $props.SelectSingleNode("md:Content", $ns)
|
||||
$contentItems = @()
|
||||
if ($contentEl -and $contentEl.HasChildNodes) {
|
||||
$xrItems = $contentEl.SelectNodes("xr:Item", $ns)
|
||||
$contentOk = $true
|
||||
foreach ($item in $xrItems) {
|
||||
$typeAttr = $item.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance")
|
||||
$text = $item.InnerText.Trim()
|
||||
$contentItems += $text
|
||||
if ($typeAttr -ne "xr:MDObjectRef") {
|
||||
Report-Error "6. Content item `"$text`": xsi:type=`"$typeAttr`" (expected xr:MDObjectRef)"
|
||||
$contentOk = $false
|
||||
}
|
||||
if ($text -notmatch '^[A-Za-z]+\..+$' -and $text -notmatch $guidPattern) {
|
||||
Report-Error "6. Content item `"$text`": invalid format (expected Type.Name or UUID)"
|
||||
$contentOk = $false
|
||||
}
|
||||
}
|
||||
if ($contentOk) { Report-OK "6. Content: $($xrItems.Count) items, all valid MDObjectRef format" }
|
||||
} else {
|
||||
Report-OK "6. Content: empty (no items)"
|
||||
}
|
||||
|
||||
# --- 7. Content duplicates ---
|
||||
if ($contentItems.Count -gt 0) {
|
||||
$dupes = $contentItems | Group-Object | Where-Object { $_.Count -gt 1 }
|
||||
if ($dupes) {
|
||||
$dupeNames = ($dupes | ForEach-Object { $_.Name }) -join ", "
|
||||
Report-Warn "7. Content: duplicates found: $dupeNames"
|
||||
} else {
|
||||
Report-OK "7. Content: no duplicates"
|
||||
}
|
||||
} else {
|
||||
Report-OK "7. Content: no duplicates (empty)"
|
||||
}
|
||||
|
||||
# --- 8. ChildObjects entries non-empty ---
|
||||
$childObjs = $sub.SelectSingleNode("md:ChildObjects", $ns)
|
||||
$childNames = @()
|
||||
if ($childObjs -and $childObjs.HasChildNodes) {
|
||||
$childOk = $true
|
||||
foreach ($child in $childObjs.ChildNodes) {
|
||||
if ($child.NodeType -ne 'Element') { continue }
|
||||
if ($child.LocalName -ne "Subsystem") {
|
||||
Report-Error "8. ChildObjects: unexpected element <$($child.LocalName)>"
|
||||
$childOk = $false
|
||||
} elseif (-not $child.InnerText.Trim()) {
|
||||
Report-Error "8. ChildObjects: empty <Subsystem> element"
|
||||
$childOk = $false
|
||||
} else {
|
||||
$childNames += $child.InnerText.Trim()
|
||||
}
|
||||
}
|
||||
if ($childOk) { Report-OK "8. ChildObjects: $($childNames.Count) entries, all non-empty" }
|
||||
} else {
|
||||
Report-OK "8. ChildObjects: empty (leaf subsystem)"
|
||||
}
|
||||
|
||||
# --- 9. ChildObjects duplicates ---
|
||||
if ($childNames.Count -gt 0) {
|
||||
$dupes = $childNames | Group-Object | Where-Object { $_.Count -gt 1 }
|
||||
if ($dupes) {
|
||||
$dupeNames = ($dupes | ForEach-Object { $_.Name }) -join ", "
|
||||
Report-Error "9. ChildObjects: duplicates: $dupeNames"
|
||||
} else {
|
||||
Report-OK "9. ChildObjects: no duplicates"
|
||||
}
|
||||
} else {
|
||||
Report-OK "9. ChildObjects: no duplicates (empty)"
|
||||
}
|
||||
|
||||
# --- 10. ChildObjects files exist ---
|
||||
if ($childNames.Count -gt 0) {
|
||||
$parentDir = [System.IO.Path]::GetDirectoryName($resolvedPath)
|
||||
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($resolvedPath)
|
||||
$subsDir = Join-Path (Join-Path $parentDir $baseName) "Subsystems"
|
||||
$missingFiles = @()
|
||||
foreach ($cn in $childNames) {
|
||||
$childXml = Join-Path $subsDir "$cn.xml"
|
||||
if (-not (Test-Path $childXml)) { $missingFiles += $cn }
|
||||
}
|
||||
if ($missingFiles.Count -eq 0) {
|
||||
Report-OK "10. ChildObjects files: all $($childNames.Count) files exist"
|
||||
} else {
|
||||
Report-Warn "10. ChildObjects files: missing: $($missingFiles -join ', ')"
|
||||
}
|
||||
} else {
|
||||
Report-OK "10. ChildObjects files: n/a (no children)"
|
||||
}
|
||||
|
||||
# --- 11. CommandInterface.xml ---
|
||||
$parentDir2 = [System.IO.Path]::GetDirectoryName($resolvedPath)
|
||||
$baseName2 = [System.IO.Path]::GetFileNameWithoutExtension($resolvedPath)
|
||||
$ciPath = Join-Path (Join-Path (Join-Path $parentDir2 $baseName2) "Ext") "CommandInterface.xml"
|
||||
if (Test-Path $ciPath) {
|
||||
try {
|
||||
[xml]$ciDoc = Get-Content -Path $ciPath -Encoding UTF8
|
||||
Report-OK "11. CommandInterface: exists, well-formed"
|
||||
} catch {
|
||||
Report-Warn "11. CommandInterface: exists but NOT well-formed: $($_.Exception.Message)"
|
||||
}
|
||||
} else {
|
||||
Report-OK "11. CommandInterface: not present"
|
||||
}
|
||||
|
||||
# --- 12. Picture format ---
|
||||
$picEl = $props.SelectSingleNode("md:Picture", $ns)
|
||||
if ($picEl -and $picEl.HasChildNodes) {
|
||||
$picRef = $picEl.SelectSingleNode("xr:Ref", $ns)
|
||||
if ($picRef -and $picRef.InnerText) {
|
||||
$refText = $picRef.InnerText
|
||||
if ($refText -match '^CommonPicture\.') {
|
||||
Report-OK "12. Picture: $refText"
|
||||
} else {
|
||||
Report-Warn "12. Picture: `"$refText`" (expected CommonPicture.XXX)"
|
||||
}
|
||||
} else {
|
||||
Report-Warn "12. Picture: has children but no xr:Ref content"
|
||||
}
|
||||
} else {
|
||||
Report-OK "12. Picture: empty (not set)"
|
||||
}
|
||||
|
||||
# --- 13. UseOneCommand constraint ---
|
||||
$useOne = $boolVals["UseOneCommand"]
|
||||
if ($useOne -eq "true") {
|
||||
if ($contentItems.Count -eq 1) {
|
||||
Report-OK "13. UseOneCommand: true, Content has exactly 1 item"
|
||||
} else {
|
||||
Report-Warn "13. UseOneCommand: true but Content has $($contentItems.Count) items (expected 1)"
|
||||
}
|
||||
} else {
|
||||
Report-OK "13. UseOneCommand: false (no constraint)"
|
||||
}
|
||||
}
|
||||
|
||||
# --- Finalize ---
|
||||
Out-Line "---"
|
||||
Out-Line "Errors: $($script:errors), Warnings: $($script:warnings)"
|
||||
|
||||
$result = $script:output.ToString()
|
||||
Write-Host $result
|
||||
|
||||
if ($OutFile) {
|
||||
if (-not [System.IO.Path]::IsPathRooted($OutFile)) {
|
||||
$OutFile = Join-Path (Get-Location).Path $OutFile
|
||||
}
|
||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||
[System.IO.File]::WriteAllText($OutFile, $result, $utf8Bom)
|
||||
Write-Host "Written to: $OutFile"
|
||||
}
|
||||
|
||||
if ($script:errors -gt 0) { exit 1 } else { exit 0 }
|
||||
@@ -29,6 +29,8 @@
|
||||
| Роли (Role) | 3 навыка `/role-*` | Анализ прав роли, создание из JSON DSL, валидация | [Подробнее](docs/role-guide.md) |
|
||||
| Схема компоновки (СКД) | 4 навыка `/skd-*` | Анализ, генерация из JSON DSL, точечное редактирование, валидация схем компоновки данных | [Подробнее](docs/skd-guide.md) |
|
||||
| Метаданные конфигурации | 4 навыка `/meta-*` | Создание, анализ, редактирование, валидация объектов метаданных (23 типа) | [Подробнее](docs/meta-guide.md) |
|
||||
| Подсистемы (Subsystem) | 4 навыка `/subsystem-*` | Анализ, создание, редактирование, валидация подсистем конфигурации | — |
|
||||
| Командный интерфейс (CI) | 2 навыка `/interface-*` | Редактирование и валидация CommandInterface.xml подсистем | — |
|
||||
| Утилиты | `/img-grid` | Наложение сетки на изображение для определения пропорций колонок | — |
|
||||
|
||||
## Требования
|
||||
@@ -51,6 +53,7 @@
|
||||
- [Схема компоновки данных (DCS)](docs/1c-dcs-spec.md) — XML-формат DataCompositionSchema, 930 схем проанализировано
|
||||
- [SKD DSL](docs/skd-dsl-spec.md) — JSON-формат описания СКД для `/skd-compile`
|
||||
- [Объекты конфигурации](docs/1c-config-objects-spec.md) — XML-формат объектов метаданных конфигурации (23 типа)
|
||||
- [Подсистемы и командный интерфейс](docs/1c-subsystem-spec.md) — XML-формат подсистем, CommandInterface.xml, секции видимости/размещения/порядка
|
||||
|
||||
## Структура репозитория
|
||||
|
||||
@@ -90,6 +93,12 @@
|
||||
├── meta-compile/ # Создание объекта метаданных
|
||||
├── meta-edit/ # Редактирование объекта метаданных
|
||||
├── meta-validate/ # Валидация объекта метаданных
|
||||
├── subsystem-info/ # Анализ структуры подсистемы
|
||||
├── subsystem-compile/ # Создание подсистемы из JSON
|
||||
├── subsystem-edit/ # Редактирование подсистемы
|
||||
├── subsystem-validate/ # Валидация подсистемы
|
||||
├── interface-edit/ # Редактирование CommandInterface.xml
|
||||
├── interface-validate/ # Валидация CommandInterface.xml
|
||||
└── img-grid/ # Сетка для анализа изображений
|
||||
docs/
|
||||
├── epf-guide.md # Гайд: внешние обработки и отчёты
|
||||
@@ -111,5 +120,6 @@ docs/
|
||||
├── 1c-role-spec.md # Спецификация ролей (Rights.xml)
|
||||
├── 1c-dcs-spec.md # Спецификация СКД (DataCompositionSchema)
|
||||
├── skd-dsl-spec.md # Спецификация SKD DSL
|
||||
└── role-dsl-spec.md # Спецификация Role DSL
|
||||
├── role-dsl-spec.md # Спецификация Role DSL
|
||||
└── 1c-subsystem-spec.md # Спецификация подсистем и командного интерфейса
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user