mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-09-04 09:10:52 +03:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be1b622fc4 |
@@ -1,32 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "cc-1c-skills",
|
|
||||||
"interface": {
|
|
||||||
"displayName": "1C Skills"
|
|
||||||
},
|
|
||||||
"plugins": [
|
|
||||||
{
|
|
||||||
"name": "1c-skills",
|
|
||||||
"source": {
|
|
||||||
"source": "url",
|
|
||||||
"url": "https://github.com/Nikolay-Shirokov/cc-1c-skills.git",
|
|
||||||
"ref": "port-codex"
|
|
||||||
},
|
|
||||||
"policy": {
|
|
||||||
"installation": "AVAILABLE"
|
|
||||||
},
|
|
||||||
"category": "Development"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "1c-skills-py",
|
|
||||||
"source": {
|
|
||||||
"source": "url",
|
|
||||||
"url": "https://github.com/Nikolay-Shirokov/cc-1c-skills.git",
|
|
||||||
"ref": "port-codex-py"
|
|
||||||
},
|
|
||||||
"policy": {
|
|
||||||
"installation": "AVAILABLE"
|
|
||||||
},
|
|
||||||
"category": "Development"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://json.schemastore.org/claude-code-marketplace-manifest.json",
|
|
||||||
"name": "cc-1c-skills",
|
|
||||||
"description": "Маркетплейс навыков для разработки на платформе 1С:Предприятие",
|
|
||||||
"owner": {
|
|
||||||
"name": "Nikolay Shirokov"
|
|
||||||
},
|
|
||||||
"plugins": [
|
|
||||||
{
|
|
||||||
"name": "1c-skills",
|
|
||||||
"source": "./",
|
|
||||||
"description": "[PowerShell] Навыки для разработки на 1С:Предприятие 8.3 — абстракции над XML-форматами и CLI конфигуратора, плюс глаза и руки для тестирования через веб-клиент."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "1c-skills-py",
|
|
||||||
"source": {
|
|
||||||
"source": "github",
|
|
||||||
"repo": "Nikolay-Shirokov/cc-1c-skills",
|
|
||||||
"ref": "port-claude-code-py"
|
|
||||||
},
|
|
||||||
"description": "[Python] То же — для Linux/Mac или когда PowerShell недоступен."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
||||||
"name": "1c-skills",
|
|
||||||
"description": "[PowerShell] Навыки для разработки на 1С:Предприятие 8.3 — абстракции над XML-форматами и CLI конфигуратора, плюс глаза и руки для тестирования через веб-клиент.",
|
|
||||||
"author": {
|
|
||||||
"name": "Nikolay Shirokov"
|
|
||||||
},
|
|
||||||
"homepage": "https://github.com/Nikolay-Shirokov/cc-1c-skills",
|
|
||||||
"repository": "https://github.com/Nikolay-Shirokov/cc-1c-skills",
|
|
||||||
"license": "MIT",
|
|
||||||
"keywords": [
|
|
||||||
"1c",
|
|
||||||
"1c-dev",
|
|
||||||
"cf",
|
|
||||||
"cfe",
|
|
||||||
"epf",
|
|
||||||
"erf",
|
|
||||||
"metadata",
|
|
||||||
"configuration",
|
|
||||||
"extension",
|
|
||||||
"form",
|
|
||||||
"report",
|
|
||||||
"skd",
|
|
||||||
"data-processor",
|
|
||||||
"mxl",
|
|
||||||
"web-client",
|
|
||||||
"testing",
|
|
||||||
"test-automation"
|
|
||||||
],
|
|
||||||
"skills": "./.claude/skills/"
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
---
|
|
||||||
name: form-add
|
|
||||||
description: Добавить пустую управляемую форму к объекту 1С. Используй когда нужно создать у объекта новую форму
|
|
||||||
argument-hint: <ObjectPath> <FormName> [Purpose] [--set-default]
|
|
||||||
allowed-tools:
|
|
||||||
- Bash
|
|
||||||
- Read
|
|
||||||
- Write
|
|
||||||
- Edit
|
|
||||||
- Glob
|
|
||||||
- Grep
|
|
||||||
---
|
|
||||||
|
|
||||||
# /form-add — Добавление формы к объекту конфигурации
|
|
||||||
|
|
||||||
Создаёт управляемую форму (metadata XML + Form.xml + Module.bsl) и регистрирует её в корневом XML объекта конфигурации (Document, Catalog, InformationRegister и др.).
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```
|
|
||||||
/form-add <ObjectPath> <FormName> [Purpose] [Synonym] [--set-default]
|
|
||||||
```
|
|
||||||
|
|
||||||
| Параметр | Обязательный | По умолчанию | Описание |
|
|
||||||
|-------------|:------------:|--------------|----------------------------------------------|
|
|
||||||
| ObjectPath | да | — | Путь к XML-файлу объекта (Documents/Док.xml) |
|
|
||||||
| FormName | да | — | Имя формы (ФормаДокумента) |
|
|
||||||
| Purpose | нет | Object | Назначение: Object, List, Choice, Record |
|
|
||||||
| Synonym | нет | = FormName | Синоним формы |
|
|
||||||
| --set-default | нет | авто | Установить как форму по умолчанию |
|
|
||||||
|
|
||||||
## Команда
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-add.ps1" -ObjectPath "<ObjectPath>" -FormName "<FormName>" [-Purpose "<Purpose>"] [-Synonym "<Synonym>"] [-SetDefault]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Purpose — назначение формы
|
|
||||||
|
|
||||||
| Purpose | Допустимые типы объектов | Основной реквизит | DefaultForm-свойство |
|
|
||||||
|---------|-------------------------|-------------------|---------------------|
|
|
||||||
| Object | Document, Catalog, DataProcessor, Report, ExternalDataProcessor, ExternalReport, ChartOf*, ExchangePlan, BusinessProcess, Task | Объект (тип: *Object.Имя) | DefaultObjectForm (DefaultForm для DataProcessor/Report/ExternalDataProcessor/ExternalReport) |
|
|
||||||
| List | Все кроме DataProcessor | Список (DynamicList) | DefaultListForm |
|
|
||||||
| Choice | Document, Catalog, ChartOf*, ExchangePlan, BusinessProcess, Task | Список (DynamicList) | DefaultChoiceForm |
|
|
||||||
| Record | InformationRegister | Запись (InformationRegisterRecordManager) | DefaultRecordForm |
|
|
||||||
|
|
||||||
## Примеры
|
|
||||||
|
|
||||||
```
|
|
||||||
# Форма документа
|
|
||||||
/form-add Documents/АвансовыйОтчет.xml ФормаДокумента --purpose Object
|
|
||||||
|
|
||||||
# Форма списка каталога
|
|
||||||
/form-add Catalogs/Контрагенты.xml ФормаСписка --purpose List
|
|
||||||
|
|
||||||
# Форма записи регистра сведений
|
|
||||||
/form-add InformationRegisters/КурсыВалют.xml ФормаЗаписи --purpose Record
|
|
||||||
|
|
||||||
# Форма выбора с синонимом
|
|
||||||
/form-add Catalogs/Номенклатура.xml ФормаВыбора --purpose Choice --synonym "Выбор номенклатуры"
|
|
||||||
|
|
||||||
# Установить как форму по умолчанию
|
|
||||||
/form-add Documents/Заказ.xml ФормаДокументаНовая --purpose Object --set-default
|
|
||||||
```
|
|
||||||
|
|
||||||
## Workflow
|
|
||||||
|
|
||||||
1. `/form-add` — создать каркас формы
|
|
||||||
2. `/form-compile` или `/form-edit` — наполнить Form.xml элементами
|
|
||||||
3. `/form-validate` — проверить корректность
|
|
||||||
4. `/form-info` — проанализировать результат
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
# form-remove v1.9 — Remove form from 1C object
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[Alias("ProcessorName")]
|
|
||||||
[string]$ObjectName,
|
|
||||||
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[string]$FormName,
|
|
||||||
|
|
||||||
[string]$SrcDir = "src"
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
|
|
||||||
# --- Проверки ---
|
|
||||||
|
|
||||||
$rootXmlPath = Join-Path $SrcDir "$ObjectName.xml"
|
|
||||||
if (-not (Test-Path $rootXmlPath)) {
|
|
||||||
Write-Error "Корневой файл обработки не найден: $rootXmlPath"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
$processorDir = Join-Path $SrcDir $ObjectName
|
|
||||||
$formsDir = Join-Path $processorDir "Forms"
|
|
||||||
$formMetaPath = Join-Path $formsDir "$FormName.xml"
|
|
||||||
$formDir = Join-Path $formsDir $FormName
|
|
||||||
|
|
||||||
if (-not (Test-Path $formMetaPath)) {
|
|
||||||
Write-Error "Метаданные формы не найдены: $formMetaPath"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Удаление файлов ---
|
|
||||||
|
|
||||||
if (Test-Path $formDir) {
|
|
||||||
Remove-Item -Path $formDir -Recurse -Force
|
|
||||||
Write-Host "[OK] Удалён каталог: $formDir"
|
|
||||||
}
|
|
||||||
|
|
||||||
Remove-Item -Path $formMetaPath -Force
|
|
||||||
Write-Host "[OK] Удалён файл: $formMetaPath"
|
|
||||||
|
|
||||||
# --- Модификация корневого XML ---
|
|
||||||
|
|
||||||
$rootXmlFull = Resolve-Path $rootXmlPath
|
|
||||||
$xmlDoc = New-Object System.Xml.XmlDocument
|
|
||||||
$xmlDoc.PreserveWhitespace = $true
|
|
||||||
$xmlDoc.Load($rootXmlFull.Path)
|
|
||||||
|
|
||||||
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
|
|
||||||
$nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
|
||||||
|
|
||||||
# Удалить <Form>FormName</Form> из ChildObjects
|
|
||||||
$formNodes = $xmlDoc.SelectNodes("//md:ChildObjects/md:Form", $nsMgr)
|
|
||||||
foreach ($node in $formNodes) {
|
|
||||||
if ($node.InnerText -eq $FormName) {
|
|
||||||
$parent = $node.ParentNode
|
|
||||||
# Удалить предшествующий whitespace
|
|
||||||
$prev = $node.PreviousSibling
|
|
||||||
if ($prev -and $prev.NodeType -eq [System.Xml.XmlNodeType]::Whitespace) {
|
|
||||||
$parent.RemoveChild($prev) | Out-Null
|
|
||||||
}
|
|
||||||
$parent.RemoveChild($node) | Out-Null
|
|
||||||
# Опустевший контейнер: остаётся отступ-whitespace, и XmlWriter пишет пару
|
|
||||||
# <ChildObjects>\n\t\t</ChildObjects>. Платформа пишет только <ChildObjects/>
|
|
||||||
# (1394 самозакрывающихся на acc+erp, пустых пар ни в одной форме — 0).
|
|
||||||
if ($parent.SelectNodes("*").Count -eq 0) { $parent.IsEmpty = $true }
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Очистить любые Default*/Auxiliary* form-слоты, указывавшие на удалённую форму
|
|
||||||
# (form-add пишет свойство по назначению: DefaultObjectForm/DefaultListForm/
|
|
||||||
# DefaultChoiceForm/DefaultRecordForm/DefaultForm — не только generic DefaultForm).
|
|
||||||
$formRefRe = "Form\.$([regex]::Escape($FormName))$"
|
|
||||||
foreach ($node in $xmlDoc.SelectNodes("//md:*", $nsMgr)) {
|
|
||||||
if ($node.LocalName -like "*Form" -and $node.InnerText -and $node.InnerText -match $formRefRe) {
|
|
||||||
# IsEmpty, а не InnerText="": пустая строка сериализуется парой <Tag></Tag>, а
|
|
||||||
# Конфигуратор пустых пар не пишет (0 на 476 942 XML корпуса) — нужен <Tag/>.
|
|
||||||
$node.IsEmpty = $true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Сохранить с BOM
|
|
||||||
$encBom = New-Object System.Text.UTF8Encoding($true)
|
|
||||||
$settings = New-Object System.Xml.XmlWriterSettings
|
|
||||||
$settings.Encoding = $encBom
|
|
||||||
$settings.Indent = $false
|
|
||||||
$settings.NewLineHandling = [System.Xml.NewLineHandling]::None
|
|
||||||
|
|
||||||
# Через MemoryStream, а не прямо в файл: нужен шаг пост-обработки строки.
|
|
||||||
$memStream = New-Object System.IO.MemoryStream
|
|
||||||
$writer = [System.Xml.XmlWriter]::Create($memStream, $settings)
|
|
||||||
$xmlDoc.Save($writer)
|
|
||||||
$writer.Flush(); $writer.Close()
|
|
||||||
|
|
||||||
$xmlText = [System.Text.Encoding]::UTF8.GetString($memStream.ToArray())
|
|
||||||
$memStream.Close()
|
|
||||||
if ($xmlText.Length -gt 0 -and $xmlText[0] -eq [char]0xFEFF) { $xmlText = $xmlText.Substring(1) }
|
|
||||||
$xmlText = $xmlText.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
|
||||||
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
|
||||||
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
|
||||||
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
|
||||||
$xmlText = [regex]::Replace($xmlText, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
|
||||||
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
|
||||||
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
|
||||||
$targetEol = if ((Test-Path -LiteralPath $rootXmlFull.Path) -and ([System.IO.File]::ReadAllText($rootXmlFull.Path) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
|
||||||
$xmlText = ($xmlText -replace "`r`n", "`n") -replace "`n", $targetEol
|
|
||||||
[System.IO.File]::WriteAllText($rootXmlFull.Path, $xmlText, $encBom)
|
|
||||||
|
|
||||||
Write-Host "[OK] Форма $FormName удалена из $rootXmlPath"
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# form-remove v1.9 — Remove form from 1C object
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from lxml import etree
|
|
||||||
|
|
||||||
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
|
||||||
# регистр не различают, в argparse совпадение точное.
|
|
||||||
def ci_parse_args(parser, argv=None):
|
|
||||||
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
|
|
||||||
argv = list(sys.argv[1:] if argv is None else argv)
|
|
||||||
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
|
|
||||||
for i, tok in enumerate(argv):
|
|
||||||
if tok.startswith('-') and tok.lower() in names:
|
|
||||||
argv[i] = names[tok.lower()]
|
|
||||||
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
|
|
||||||
choice_map = {}
|
|
||||||
for a in parser._actions:
|
|
||||||
if a.choices:
|
|
||||||
for s in a.option_strings:
|
|
||||||
choice_map[s] = {str(c).lower(): c for c in a.choices}
|
|
||||||
for i in range(len(argv) - 1):
|
|
||||||
m = choice_map.get(argv[i])
|
|
||||||
if m and argv[i + 1].lower() in m:
|
|
||||||
argv[i + 1] = m[argv[i + 1].lower()]
|
|
||||||
return parser.parse_args(argv)
|
|
||||||
|
|
||||||
|
|
||||||
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
|
|
||||||
|
|
||||||
|
|
||||||
def _detect_xml_style(path):
|
|
||||||
"""Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
|
|
||||||
финальный перенос. None → файл новый (сохранить текущее поведение)."""
|
|
||||||
try:
|
|
||||||
raw = open(path, "rb").read()
|
|
||||||
except OSError:
|
|
||||||
return None
|
|
||||||
bom = raw.startswith(b"\xef\xbb\xbf")
|
|
||||||
body = raw[3:] if bom else raw
|
|
||||||
crlf = b"\r\n" in body
|
|
||||||
m = re.search(rb'encoding="([^"]+)"', body[:200])
|
|
||||||
enc = m.group(1).decode("ascii") if m else "utf-8"
|
|
||||||
final_nl = body.endswith(b"\n")
|
|
||||||
return {"bom": bom, "crlf": crlf, "enc": enc, "final_nl": final_nl}
|
|
||||||
|
|
||||||
|
|
||||||
def _finalize_xml_bytes(xml_bytes, style):
|
|
||||||
"""Привести байты к стилю оригинала; для НОВОГО файла (style is None) — к канону
|
|
||||||
выгрузки Конфигуратора: encoding="UTF-8", CRLF в разделителях, без перевода в конце."""
|
|
||||||
enc_decl = style["enc"] if style else "UTF-8"
|
|
||||||
xml_bytes = xml_bytes.replace(
|
|
||||||
b"<?xml version='1.0' encoding='UTF-8'?>",
|
|
||||||
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
|
|
||||||
# Канонизировать переносы к LF (убирает от \r в tail'ах)
|
|
||||||
xml_bytes = (xml_bytes.replace(b" \n", b"\n").replace(b" ", b"")
|
|
||||||
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
|
|
||||||
# Финальный перенос — как в оригинале (новый файл → нет, канон #57)
|
|
||||||
want_final_nl = style["final_nl"] if style else False
|
|
||||||
xml_bytes = xml_bytes.rstrip(b"\n")
|
|
||||||
if want_final_nl:
|
|
||||||
xml_bytes += b"\n"
|
|
||||||
# EOL — как в оригинале (новый файл → CRLF, канон #57)
|
|
||||||
if (style["crlf"] if style else True):
|
|
||||||
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
|
|
||||||
return xml_bytes
|
|
||||||
|
|
||||||
|
|
||||||
def save_xml_with_bom(tree, path):
|
|
||||||
"""Save XML tree preserving the existing file's BOM/EOL/encoding-case/final-newline."""
|
|
||||||
style = _detect_xml_style(path)
|
|
||||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
|
||||||
xml_bytes = _finalize_xml_bytes(xml_bytes, style)
|
|
||||||
with open(path, "wb") as f:
|
|
||||||
if style is None or style["bom"]:
|
|
||||||
f.write(b"\xef\xbb\xbf")
|
|
||||||
f.write(xml_bytes)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(description="Remove form from 1C object", allow_abbrev=False)
|
|
||||||
parser.add_argument("-ObjectName", "-ProcessorName", required=True)
|
|
||||||
parser.add_argument("-FormName", required=True)
|
|
||||||
parser.add_argument("-SrcDir", default="src")
|
|
||||||
args = ci_parse_args(parser)
|
|
||||||
|
|
||||||
object_name = args.ObjectName
|
|
||||||
form_name = args.FormName
|
|
||||||
src_dir = args.SrcDir
|
|
||||||
|
|
||||||
# --- Checks ---
|
|
||||||
|
|
||||||
root_xml_path = os.path.join(src_dir, f"{object_name}.xml")
|
|
||||||
if not os.path.exists(root_xml_path):
|
|
||||||
print(f"Корневой файл обработки не найден: {root_xml_path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
processor_dir = os.path.join(src_dir, object_name)
|
|
||||||
forms_dir = os.path.join(processor_dir, "Forms")
|
|
||||||
form_meta_path = os.path.join(forms_dir, f"{form_name}.xml")
|
|
||||||
form_dir = os.path.join(forms_dir, form_name)
|
|
||||||
|
|
||||||
if not os.path.exists(form_meta_path):
|
|
||||||
print(f"Метаданные формы не найдены: {form_meta_path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Delete files ---
|
|
||||||
|
|
||||||
if os.path.isdir(form_dir):
|
|
||||||
shutil.rmtree(form_dir)
|
|
||||||
print(f"[OK] Удалён каталог: {form_dir}")
|
|
||||||
|
|
||||||
os.remove(form_meta_path)
|
|
||||||
print(f"[OK] Удалён файл: {form_meta_path}")
|
|
||||||
|
|
||||||
# --- Modify root XML ---
|
|
||||||
|
|
||||||
root_xml_full = os.path.abspath(root_xml_path)
|
|
||||||
parser_xml = etree.XMLParser(remove_blank_text=False)
|
|
||||||
tree = etree.parse(root_xml_full, parser_xml)
|
|
||||||
root = tree.getroot()
|
|
||||||
|
|
||||||
# Remove <Form>FormName</Form> from ChildObjects
|
|
||||||
for node in root.findall(".//md:ChildObjects/md:Form", NSMAP):
|
|
||||||
if node.text and node.text.strip() == form_name:
|
|
||||||
parent = node.getparent()
|
|
||||||
prev = node.getprevious()
|
|
||||||
if prev is not None:
|
|
||||||
# Whitespace is in prev.tail
|
|
||||||
if prev.tail and prev.tail.strip() == "":
|
|
||||||
prev.tail = ""
|
|
||||||
else:
|
|
||||||
# First child — whitespace is in parent.text
|
|
||||||
if parent.text and parent.text.strip() == "":
|
|
||||||
parent.text = ""
|
|
||||||
parent.remove(node)
|
|
||||||
# Опустевший контейнер: text="" сериализуется парой <ChildObjects></ChildObjects>,
|
|
||||||
# а нужен <ChildObjects/> — PS-порт через DOM даёт именно его.
|
|
||||||
if len(parent) == 0 and not (parent.text or "").strip():
|
|
||||||
parent.text = None
|
|
||||||
break
|
|
||||||
|
|
||||||
# Clear any Default*/Auxiliary* form slot that pointed to the removed form
|
|
||||||
# (form-add writes the purpose-specific property: DefaultObjectForm / DefaultListForm /
|
|
||||||
# DefaultChoiceForm / DefaultRecordForm / DefaultForm — not just generic DefaultForm).
|
|
||||||
ref_re = re.compile(rf"Form\.{re.escape(form_name)}$")
|
|
||||||
for el in root.iter():
|
|
||||||
if not isinstance(el.tag, str):
|
|
||||||
continue
|
|
||||||
if etree.QName(el).localname.endswith("Form") and el.text and ref_re.search(el.text):
|
|
||||||
# text=None, а не "": пустая строка сериализуется парой <Tag></Tag>, а
|
|
||||||
# Конфигуратор пустых пар не пишет (0 на 476 942 XML корпуса) — нужен <Tag/>.
|
|
||||||
el.text = None
|
|
||||||
|
|
||||||
# Save with BOM
|
|
||||||
save_xml_with_bom(tree, root_xml_full)
|
|
||||||
|
|
||||||
print(f"[OK] Форма {form_name} удалена из {root_xml_path}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -24,7 +24,7 @@ allowed-tools:
|
|||||||
| `NoValidate` | Пропустить авто-валидацию |
|
| `NoValidate` | Пропустить авто-валидацию |
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-edit.ps1" -ConfigPath '<path>' -Operation modify-property -Value 'Version=1.0.0.1'
|
python ".gemini/skills/cf-edit/scripts/cf-edit.py" -ConfigPath '<path>' -Operation modify-property -Value 'Version=1.0.0.1'
|
||||||
```
|
```
|
||||||
|
|
||||||
## Операции
|
## Операции
|
||||||
@@ -34,6 +34,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-edit.ps1" -Confi
|
|||||||
| `modify-property` | `Ключ=Значение` (batch `;;`) | Изменить свойство |
|
| `modify-property` | `Ключ=Значение` (batch `;;`) | Изменить свойство |
|
||||||
| `add-childObject` | `Type.Name` (batch `;;`) | Зарегистрировать уже существующий файл объекта в ChildObjects. Для создания нового объекта используй `/meta-compile`, `/role-compile`, `/subsystem-compile` — они регистрируют автоматически |
|
| `add-childObject` | `Type.Name` (batch `;;`) | Зарегистрировать уже существующий файл объекта в ChildObjects. Для создания нового объекта используй `/meta-compile`, `/role-compile`, `/subsystem-compile` — они регистрируют автоматически |
|
||||||
| `remove-childObject` | `Type.Name` (batch `;;`) | Удалить объект из ChildObjects |
|
| `remove-childObject` | `Type.Name` (batch `;;`) | Удалить объект из ChildObjects |
|
||||||
|
| `sort-childObjects` | вид, напр. `Catalog` (batch `;;`), либо пусто | Упорядочить ChildObjects по имени внутри вида. Без значения — все виды, кроме четырёх (см. reference) |
|
||||||
| `add-defaultRole` | `Role.Name` или `Name` | Добавить роль по умолчанию |
|
| `add-defaultRole` | `Role.Name` или `Name` | Добавить роль по умолчанию |
|
||||||
| `remove-defaultRole` | `Role.Name` или `Name` | Удалить роль по умолчанию |
|
| `remove-defaultRole` | `Role.Name` или `Name` | Удалить роль по умолчанию |
|
||||||
| `set-defaultRoles` | Имена через `;;` | Заменить список ролей по умолчанию |
|
| `set-defaultRoles` | Имена через `;;` | Заменить список ролей по умолчанию |
|
||||||
@@ -39,6 +39,20 @@
|
|||||||
|
|
||||||
Batch: `"Catalog.Товары ;; Document.Заказ ;; Enum.ВидыОплат"`
|
Batch: `"Catalog.Товары ;; Document.Заказ ;; Enum.ВидыОплат"`
|
||||||
|
|
||||||
|
## sort-childObjects
|
||||||
|
|
||||||
|
Упорядочивает объекты в `<ChildObjects>` по имени **внутри вида**. Значение — имя вида (`Catalog`, `Role`, …), batch через `;;`. Без значения обрабатываются все виды, какие есть в файле.
|
||||||
|
|
||||||
|
```
|
||||||
|
-Operation sort-childObjects — все виды, кроме перечисленных ниже
|
||||||
|
-Operation sort-childObjects -Value "Catalog" — только справочники
|
||||||
|
-Operation sort-childObjects -Value "Catalog ;; Role"
|
||||||
|
```
|
||||||
|
|
||||||
|
Не сортируются, пока вид не назван явно: `CommonAttribute`, `Subsystem`, `CommandGroup`, `Language`.
|
||||||
|
|
||||||
|
Вызов без значения дополнительно ставит группы видов в канонический порядок; вызов с явным видом трогает только имена внутри него.
|
||||||
|
|
||||||
## add-defaultRole / remove-defaultRole / set-defaultRoles
|
## add-defaultRole / remove-defaultRole / set-defaultRoles
|
||||||
|
|
||||||
Имя роли: `ПолныеПрава` или `Role.ПолныеПрава` (префикс `Role.` добавляется автоматически).
|
Имя роли: `ПолныеПрава` или `Role.ПолныеПрава` (префикс `Role.` добавляется автоматически).
|
||||||
+364
-41
@@ -1,15 +1,80 @@
|
|||||||
# cf-edit v1.19 — Edit 1C configuration root (Configuration.xml)
|
# cf-edit v1.28 — Edit 1C configuration root (Configuration.xml)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
[CmdletBinding(PositionalBinding=$false)]
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)][Alias('Path')][string]$ConfigPath,
|
[Parameter(Mandatory)][Alias('Path')][string]$ConfigPath,
|
||||||
[string]$DefinitionFile,
|
[string]$DefinitionFile,
|
||||||
[ValidateSet("modify-property","add-childObject","remove-childObject","add-defaultRole","remove-defaultRole","set-defaultRoles","set-panels","set-home-page")]
|
[ValidateSet("modify-property","add-childObject","remove-childObject","add-defaultRole","remove-defaultRole","set-defaultRoles","set-panels","set-home-page","sort-childObjects")]
|
||||||
[string]$Operation,
|
[string]$Operation,
|
||||||
[string]$Value,
|
[string]$Value,
|
||||||
[switch]$NoValidate
|
[switch]$NoValidate
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
# --- Разбор пользовательского JSON ---
|
||||||
|
# Одна строка в stderr вместо дампа исключения ConvertFrom-Json (issue #80): агент по стектрейсу
|
||||||
|
# идёт чинить скрипт, а не свой вызов. $source — файл или параметр. $expected заполняем только
|
||||||
|
# для полиморфного входа: у файла подсказка была бы наполнителем. -Inline печатает ещё и то,
|
||||||
|
# что доехало: у файла такого вопроса нет — путь назван, позицию дал парсер, файл на диске.
|
||||||
|
# Возврат через -NoEnumerate: без него одноэлементный
|
||||||
|
# JSON-массив разворачивался бы в скаляр вторым анруллингом.
|
||||||
|
function ConvertFrom-JsonInput([string]$text, [string]$source, [string]$expected, [switch]$Inline) {
|
||||||
|
try {
|
||||||
|
# PS 5.1 на пустой строке отдаёт $null, а не ошибку — навык уходил дальше с $null,
|
||||||
|
# тогда как py-порт падал. Проверяем сами, чтобы порты вели себя одинаково.
|
||||||
|
if ([string]::IsNullOrWhiteSpace($text)) { throw 'input is empty' }
|
||||||
|
$parsed = $text | ConvertFrom-Json
|
||||||
|
} catch {
|
||||||
|
$what = if ($expected) { "$source expects $expected" } else { "Invalid JSON in $source" }
|
||||||
|
if ($Inline) {
|
||||||
|
$got = ($text -replace '\s+', ' ').Trim()
|
||||||
|
$label = 'got'
|
||||||
|
if (-not $got) { $got = '(empty)' }
|
||||||
|
elseif ($got.Length -gt 60) { $label = 'got (first 60 chars)'; $got = $got.Substring(0, 60) }
|
||||||
|
$what = "${what}, ${label}: ${got}"
|
||||||
|
}
|
||||||
|
[Console]::Error.WriteLine("[ERROR] ${what} ($($_.Exception.Message))")
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
Write-Output -NoEnumerate $parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Чтение входного JSON-файла ---
|
||||||
|
# Кодировку берём из BOM — это объявление самого файла, а не догадка. Без BOM ждём строгий UTF-8:
|
||||||
|
# Get-Content -Encoding UTF8 на файле в cp1251 тихо меняет кириллицу на U+FFFD, JSON после этого
|
||||||
|
# разбирается успешно, и в конфигурацию уезжает имя из «замен». Кодовую страницу не подбираем:
|
||||||
|
# угаданное имя уйдёт в метаданные так же молча.
|
||||||
|
function Read-JsonInputFile([string]$path) {
|
||||||
|
# Проверка здесь, а не по навыкам: часть навыков проверяла путь сама, часть — нет, и один и тот
|
||||||
|
# же промах давал то внятную строку, то дамп MethodInvocationException. Навыки со своей
|
||||||
|
# проверкой срабатывают раньше и сохраняют свой текст.
|
||||||
|
if (-not (Test-Path -LiteralPath $path)) {
|
||||||
|
[Console]::Error.WriteLine("[ERROR] File not found: $path")
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if (Test-Path -LiteralPath $path -PathType Container) {
|
||||||
|
[Console]::Error.WriteLine("[ERROR] Expected a JSON file, got a directory: $path")
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
$bytes = [System.IO.File]::ReadAllBytes($path)
|
||||||
|
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) {
|
||||||
|
return [System.Text.Encoding]::UTF8.GetString($bytes, 3, $bytes.Length - 3)
|
||||||
|
}
|
||||||
|
if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) {
|
||||||
|
return [System.Text.Encoding]::Unicode.GetString($bytes, 2, $bytes.Length - 2)
|
||||||
|
}
|
||||||
|
if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFE -and $bytes[1] -eq 0xFF) {
|
||||||
|
return [System.Text.Encoding]::BigEndianUnicode.GetString($bytes, 2, $bytes.Length - 2)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return (New-Object System.Text.UTF8Encoding($false, $true)).GetString($bytes)
|
||||||
|
} catch {
|
||||||
|
$detail = if ($_.Exception.InnerException) { $_.Exception.InnerException.Message } else { $_.Exception.Message }
|
||||||
|
[Console]::Error.WriteLine("[ERROR] ${path} is not valid UTF-8: ${detail} - save the file as UTF-8, or add a BOM if it is UTF-16")
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
# --- Mode validation ---
|
# --- Mode validation ---
|
||||||
@@ -212,14 +277,14 @@ foreach ($child in $script:propsEl.ChildNodes) {
|
|||||||
}
|
}
|
||||||
Info "Configuration: $($script:objName)"
|
Info "Configuration: $($script:objName)"
|
||||||
|
|
||||||
# --- Canonical type order for ChildObjects (44 types) ---
|
# --- Canonical type order for ChildObjects (46 types) ---
|
||||||
$script:typeOrder = @(
|
$script:typeOrder = @(
|
||||||
"Language","Subsystem","StyleItem","Style",
|
"Language","Subsystem","StyleItem","Style",
|
||||||
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
||||||
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
|
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
|
||||||
"XDTOPackage","WebService","HTTPService","WSReference",
|
"XDTOPackage","WebService","HTTPService","WSReference",
|
||||||
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
||||||
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
"FunctionalOptionsParameter","DefinedType","Bot","PaletteColor","CommonCommand","CommandGroup",
|
||||||
"Constant","CommonForm","Catalog","Document",
|
"Constant","CommonForm","Catalog","Document",
|
||||||
"DocumentNumerator","Sequence","DocumentJournal","Enum",
|
"DocumentNumerator","Sequence","DocumentJournal","Enum",
|
||||||
"Report","DataProcessor","InformationRegister","AccumulationRegister",
|
"Report","DataProcessor","InformationRegister","AccumulationRegister",
|
||||||
@@ -232,7 +297,7 @@ $script:typeOrder = @(
|
|||||||
$script:typeToDir = @{
|
$script:typeToDir = @{
|
||||||
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"
|
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"
|
||||||
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"; "CommonTemplate"="CommonTemplates"
|
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"; "CommonTemplate"="CommonTemplates"
|
||||||
"FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"; "Bot"="Bots"; "CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"
|
"FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"; "Bot"="Bots"; "PaletteColor"="PaletteColors"; "CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"
|
||||||
"XDTOPackage"="XDTOPackages"; "WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
|
"XDTOPackage"="XDTOPackages"; "WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
|
||||||
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"; "SettingsStorage"="SettingsStorages"; "FunctionalOption"="FunctionalOptions"
|
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"; "SettingsStorage"="SettingsStorages"; "FunctionalOption"="FunctionalOptions"
|
||||||
"FunctionalOptionsParameter"="FunctionalOptionsParameters"; "DefinedType"="DefinedTypes"; "CommonCommand"="CommonCommands"; "CommandGroup"="CommandGroups"
|
"FunctionalOptionsParameter"="FunctionalOptionsParameters"; "DefinedType"="DefinedTypes"; "CommonCommand"="CommonCommands"; "CommandGroup"="CommandGroups"
|
||||||
@@ -311,6 +376,21 @@ function Import-Fragment([string]$xmlString) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- Parse batch value (split by ;;) ---
|
# --- Parse batch value (split by ;;) ---
|
||||||
|
|
||||||
|
# Имя вида из пользовательского ввода → каноническое имя или $null.
|
||||||
|
# Ввод прощающий: регистр не важен, принимается имя каталога выгрузки (Catalogs → Catalog)
|
||||||
|
# и русское имя вида в единственном и множественном числе.
|
||||||
|
function Resolve-TypeName([string]$token) {
|
||||||
|
$key = "$token".Trim()
|
||||||
|
if (-not $key) { return $null }
|
||||||
|
foreach ($canon in $script:typeOrder) { if ($canon -eq $key) { return $canon } }
|
||||||
|
$byDir = $script:dirToType[$key.ToLowerInvariant()]
|
||||||
|
if ($byDir) { return $byDir }
|
||||||
|
$ru = $script:ruTypeMap[$key.ToLowerInvariant()]
|
||||||
|
if ($ru) { return $ru }
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
function Parse-BatchValue([string]$val) {
|
function Parse-BatchValue([string]$val) {
|
||||||
$items = @()
|
$items = @()
|
||||||
foreach ($part in $val.Split(";;")) {
|
foreach ($part in $val.Split(";;")) {
|
||||||
@@ -376,6 +456,220 @@ function Do-ModifyProperty([string]$batchVal) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- Operation: add-childObject ---
|
# --- Operation: add-childObject ---
|
||||||
|
# Куда навык ставит новую запись в <ChildObjects> — настройка newObjectPosition.
|
||||||
|
# databases[].newObjectPosition базы, чей configSrc охватывает каталог родительского XML,
|
||||||
|
# иначе корневое поле, иначе end. Значения: end — после последнего объекта того же вида
|
||||||
|
# (так дописывает Конфигуратор); byName — по имени среди объектов того же вида.
|
||||||
|
# Файл ищем от рабочего каталога вверх, каталог конфигурации — запасной путь: так же
|
||||||
|
# его ищут support-guard и группа db-*, а скрипт навыка зовут по абсолютному пути, и cwd
|
||||||
|
# остаётся рабочим каталогом проекта.
|
||||||
|
# configSrc считается от каталога .v8-project.json, как задокументировано в
|
||||||
|
# docs/v8-project-guide.md. Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||||
|
function Get-NewObjectPosition([string]$cfgDir) {
|
||||||
|
try {
|
||||||
|
if (-not $cfgDir) { $cfgDir = "." }
|
||||||
|
$pj = Find-V8Project (Get-Location).Path
|
||||||
|
if (-not $pj) { $pj = Find-V8Project ([System.IO.Path]::GetFullPath($cfgDir)) }
|
||||||
|
if (-not $pj) { return "end" }
|
||||||
|
$proj = Get-Content -Raw $pj | ConvertFrom-Json
|
||||||
|
$projDir = [System.IO.Path]::GetDirectoryName($pj)
|
||||||
|
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
|
||||||
|
if ($proj.databases) {
|
||||||
|
foreach ($db in $proj.databases) {
|
||||||
|
if ($db.configSrc -and $db.newObjectPosition) {
|
||||||
|
$src = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($projDir, $db.configSrc)).TrimEnd('\', '/')
|
||||||
|
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
|
||||||
|
if ("$($db.newObjectPosition)" -eq "byName") { return "byName" }
|
||||||
|
return "end"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ("$($proj.newObjectPosition)" -eq "byName") { return "byName" }
|
||||||
|
return "end"
|
||||||
|
} catch { return "end" }
|
||||||
|
}
|
||||||
|
|
||||||
|
# Виды, у которых порядок в дереве несёт смысл: автоматически их не упорядочиваем.
|
||||||
|
# CommonAttribute — исключение самого стандарта (#std467): у общих реквизитов-разделителей
|
||||||
|
# порядок в дереве задаёт порядок установки параметров сеанса. Subsystem и CommandGroup:
|
||||||
|
# пока они не перечислены в <SubsystemsOrder> / <GroupsOrder> файла Ext/CommandInterface.xml,
|
||||||
|
# порядок дерева задаёт порядок в интерфейсе, а платформа эти списки сама не заводит
|
||||||
|
# (в выгрузке ACC вне GroupsOrder 15 живых групп из 39). Language исключён из осторожности,
|
||||||
|
# без замера: языков обычно один-два, и в типовых их порядок не алфавитный.
|
||||||
|
# Явно названный вид сортируется в любом случае.
|
||||||
|
# Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||||
|
function Test-OrderSensitiveType([string]$typeName) {
|
||||||
|
return @("CommonAttribute", "Subsystem", "CommandGroup", "Language") -ccontains $typeName
|
||||||
|
}
|
||||||
|
|
||||||
|
# Порядок имён объектов метаданных, как в дереве Конфигуратора.
|
||||||
|
# Ключ — пары «ранг+символ»: регистр не учитывается, подчёркивание раньше цифр, цифры раньше
|
||||||
|
# букв, буквы по кодам (латиница раньше кириллицы), ё на месте е. Культурные таблицы не
|
||||||
|
# используются — они разные на разных ОС и в разных рантаймах, а так оба порта сравнивают
|
||||||
|
# одинаково везде. Равные ключи разводит ordinal-сравнение исходных строк.
|
||||||
|
# Возвращает -1 | 0 | 1. Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||||
|
function Compare-MetadataNames([string]$a, [string]$b) {
|
||||||
|
$keys = @("", "")
|
||||||
|
$names = @($a, $b)
|
||||||
|
for ($i = 0; $i -lt 2; $i++) {
|
||||||
|
$sb = New-Object System.Text.StringBuilder
|
||||||
|
foreach ($ch in $names[$i].ToLowerInvariant().ToCharArray()) {
|
||||||
|
if ($ch -eq [char]0x0451) { $ch = [char]0x0435 }
|
||||||
|
if ([char]::IsDigit($ch)) { [void]$sb.Append('1') }
|
||||||
|
elseif ([char]::IsLetter($ch)) { [void]$sb.Append('2') }
|
||||||
|
else { [void]$sb.Append('0') }
|
||||||
|
[void]$sb.Append($ch)
|
||||||
|
}
|
||||||
|
$keys[$i] = $sb.ToString()
|
||||||
|
}
|
||||||
|
$r = [string]::CompareOrdinal($keys[0], $keys[1])
|
||||||
|
if ($r -eq 0) { $r = [string]::CompareOrdinal($a, $b) }
|
||||||
|
if ($r -lt 0) { return -1 }
|
||||||
|
if ($r -gt 0) { return 1 }
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Сортировка имён компаратором Compare-MetadataNames. В py-порту ту же роль играет
|
||||||
|
# functools.cmp_to_key — штатный способ отсортировать компаратором; в PS 5.1 его нет,
|
||||||
|
# поэтому слияние вручную. Порядок обоих портов задаёт один и тот же компаратор.
|
||||||
|
function Sort-MetadataNames([string[]]$names) {
|
||||||
|
# Возврат без запятой-обёртки: приёмная сторона всегда пишет @(...), и одноэлементный
|
||||||
|
# результат остаётся массивом. С `return ,@(...)` @() собрал бы ОДИН объект-массив.
|
||||||
|
if ($names.Count -le 1) { return $names }
|
||||||
|
$mid = [int]($names.Count / 2)
|
||||||
|
$left = @(Sort-MetadataNames $names[0..($mid - 1)])
|
||||||
|
$right = @(Sort-MetadataNames $names[$mid..($names.Count - 1)])
|
||||||
|
$out = New-Object System.Collections.ArrayList
|
||||||
|
$i = 0; $j = 0
|
||||||
|
while ($i -lt $left.Count -and $j -lt $right.Count) {
|
||||||
|
if ((Compare-MetadataNames $left[$i] $right[$j]) -le 0) { [void]$out.Add($left[$i]); $i++ }
|
||||||
|
else { [void]$out.Add($right[$j]); $j++ }
|
||||||
|
}
|
||||||
|
while ($i -lt $left.Count) { [void]$out.Add($left[$i]); $i++ }
|
||||||
|
while ($j -lt $right.Count) { [void]$out.Add($right[$j]); $j++ }
|
||||||
|
return $out.ToArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Упорядочить <ChildObjects>: имена внутри вида, а без аргумента — и группы видов.
|
||||||
|
# Виды из Test-OrderSensitiveType по имени не сортируются, пока не названы явно.
|
||||||
|
# Вызов без значения дополнительно ставит группы видов в канонический порядок: платформа
|
||||||
|
# починила бы его только при загрузке-выгрузке, то есть неканоничный файл даёт диф на
|
||||||
|
# ровном месте. Переставляем ЗНАЧЕНИЯ узлов, а не сами узлы — отступы и структура файла
|
||||||
|
# остаются как были, в дифе только перестановка строк.
|
||||||
|
function Do-SortChildObjects([string]$batchVal) {
|
||||||
|
if (-not $script:childObjsEl) { Write-Error "No <ChildObjects> element found"; exit 1 }
|
||||||
|
|
||||||
|
# Ввод прощающий: регистр не важен, принимается и имя каталога (Catalogs → Catalog) —
|
||||||
|
# в дереве выгрузки виды видны именно во множественном числе.
|
||||||
|
# Без @(...) на приёме: Parse-BatchValue возвращает ,$items — обёртка, которую @()
|
||||||
|
# собрал бы как ОДИН объект-массив, и вид не нашёлся бы в $script:typeOrder.
|
||||||
|
$tokens = @()
|
||||||
|
if ("$batchVal".Trim()) { $tokens = Parse-BatchValue $batchVal }
|
||||||
|
$requested = @()
|
||||||
|
foreach ($token in $tokens) {
|
||||||
|
$canon = Resolve-TypeName $token
|
||||||
|
if (-not $canon) { Write-Error "Unknown type '$token'. Valid: $($script:typeOrder -join ', ')"; exit 1 }
|
||||||
|
$requested += $canon
|
||||||
|
}
|
||||||
|
|
||||||
|
$groups = New-Object System.Collections.Specialized.OrderedDictionary
|
||||||
|
foreach ($child in $script:childObjsEl.ChildNodes) {
|
||||||
|
if ($child.NodeType -ne 'Element') { continue }
|
||||||
|
$ln = $child.get_LocalName()
|
||||||
|
if (-not $groups.Contains($ln)) { $groups[$ln] = New-Object System.Collections.ArrayList }
|
||||||
|
[void]$groups[$ln].Add($child)
|
||||||
|
}
|
||||||
|
|
||||||
|
$targets = if ($requested.Count -gt 0) { $requested } else { @($groups.Keys | Where-Object { -not (Test-OrderSensitiveType $_) }) }
|
||||||
|
foreach ($typeName in $targets) {
|
||||||
|
if (-not $groups.Contains($typeName)) { continue }
|
||||||
|
$els = $groups[$typeName]
|
||||||
|
if ($els.Count -lt 2) { continue }
|
||||||
|
$names = @(foreach ($e in $els) { $e.InnerText })
|
||||||
|
$ordered = @(Sort-MetadataNames $names)
|
||||||
|
$same = $true
|
||||||
|
for ($i = 0; $i -lt $names.Count; $i++) { if ($names[$i] -cne $ordered[$i]) { $same = $false; break } }
|
||||||
|
if ($same) { continue }
|
||||||
|
for ($i = 0; $i -lt $els.Count; $i++) { $els[$i].InnerText = $ordered[$i] }
|
||||||
|
$script:modifyCount++
|
||||||
|
Info "Sorted: $typeName ($($els.Count))"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($requested.Count -gt 0) { return }
|
||||||
|
|
||||||
|
# Без аргумента приводим в порядок и сами группы видов: собранная навыками конфигурация
|
||||||
|
# может держать их не в каноне, и первая же выгрузка платформы даст диф. Переставляем
|
||||||
|
# содержимое существующих узлов, а не узлы, поэтому отступы и структура файла не меняются —
|
||||||
|
# в дифе только перестановка строк. Имя тега у XmlElement неизменяемо, поэтому там, где вид
|
||||||
|
# меняется, узел заменяется через ReplaceChild: он сохраняет окружающие пробельные узлы.
|
||||||
|
$elems = @()
|
||||||
|
foreach ($child in $script:childObjsEl.ChildNodes) {
|
||||||
|
if ($child.NodeType -eq 'Element') { $elems += $child }
|
||||||
|
}
|
||||||
|
$tags = @(); $texts = @()
|
||||||
|
foreach ($e in $elems) { $tags += $e.get_LocalName(); $texts += $e.InnerText }
|
||||||
|
$rank = @()
|
||||||
|
for ($i = 0; $i -lt $tags.Count; $i++) {
|
||||||
|
$r = $script:typeOrder.IndexOf($tags[$i])
|
||||||
|
if ($r -lt 0) { $r = $script:typeOrder.Count }
|
||||||
|
$rank += $r
|
||||||
|
}
|
||||||
|
# Порядок стабильный: вторым ключом идёт исходная позиция
|
||||||
|
$order = @(0..($tags.Count - 1) | Sort-Object @{e={$rank[$_]}}, @{e={$_}})
|
||||||
|
$same = $true
|
||||||
|
for ($i = 0; $i -lt $order.Count; $i++) { if ($order[$i] -ne $i) { $same = $false; break } }
|
||||||
|
if ($same) { return }
|
||||||
|
|
||||||
|
for ($i = 0; $i -lt $elems.Count; $i++) {
|
||||||
|
$srcIdx = $order[$i]
|
||||||
|
if ($tags[$i] -ceq $tags[$srcIdx]) {
|
||||||
|
$elems[$i].InnerText = $texts[$srcIdx]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
$newEl = $script:xmlDoc.CreateElement($tags[$srcIdx], $script:mdNs)
|
||||||
|
$newEl.InnerText = $texts[$srcIdx]
|
||||||
|
[void]$script:childObjsEl.ReplaceChild($newEl, $elems[$i])
|
||||||
|
}
|
||||||
|
$script:modifyCount++
|
||||||
|
Info "Reordered type groups: $($elems.Count) entries"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
|
||||||
|
# финальный перенос. $null → файл новый (сохранить текущее поведение).
|
||||||
|
# Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||||
|
function Detect-XmlStyle([string]$path) {
|
||||||
|
if (-not (Test-Path -LiteralPath $path)) { return $null }
|
||||||
|
$raw = [System.IO.File]::ReadAllBytes($path)
|
||||||
|
$bom = ($raw.Length -ge 3 -and $raw[0] -eq 0xEF -and $raw[1] -eq 0xBB -and $raw[2] -eq 0xBF)
|
||||||
|
$body = if ($bom) { [System.Text.Encoding]::UTF8.GetString($raw, 3, $raw.Length - 3) } else { [System.Text.Encoding]::UTF8.GetString($raw) }
|
||||||
|
$head = if ($body.Length -gt 200) { $body.Substring(0, 200) } else { $body }
|
||||||
|
$m = [regex]::Match($head, 'encoding="([^"]+)"')
|
||||||
|
return @{
|
||||||
|
bom = $bom
|
||||||
|
crlf = $body.Contains("`r`n")
|
||||||
|
enc = $(if ($m.Success) { $m.Groups[1].Value } else { "utf-8" })
|
||||||
|
finalNl = $body.EndsWith("`n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Привести текст XmlWriter к стилю оригинала; для НОВОГО файла ($null) — к канону выгрузки
|
||||||
|
# Конфигуратора: encoding="UTF-8", CRLF, без перевода строки в конце.
|
||||||
|
# Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||||
|
function Finalize-XmlText([string]$text, $style) {
|
||||||
|
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
|
||||||
|
$encDecl = $(if ($style) { $style.enc } else { "UTF-8" })
|
||||||
|
$text = $text.Replace('encoding="utf-8"', 'encoding="' + $encDecl + '"')
|
||||||
|
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||||
|
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||||
|
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||||
|
$text = [regex]::Replace($text, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||||
|
$text = ($text -replace "`r`n", "`n").TrimEnd("`n")
|
||||||
|
if ($style -and $style.finalNl) { $text += "`n" }
|
||||||
|
if (-not $style -or $style.crlf) { $text = $text -replace "`n", "`r`n" }
|
||||||
|
return $text
|
||||||
|
}
|
||||||
|
|
||||||
function Do-AddChildObject([string]$batchVal) {
|
function Do-AddChildObject([string]$batchVal) {
|
||||||
if (-not $script:childObjsEl) { Write-Error "No <ChildObjects> element found"; exit 1 }
|
if (-not $script:childObjsEl) { Write-Error "No <ChildObjects> element found"; exit 1 }
|
||||||
|
|
||||||
@@ -395,6 +689,8 @@ function Do-AddChildObject([string]$batchVal) {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
$typeName = $item.Substring(0, $dotIdx)
|
$typeName = $item.Substring(0, $dotIdx)
|
||||||
|
$canonType = Resolve-TypeName $typeName
|
||||||
|
if ($canonType) { $typeName = $canonType }
|
||||||
$objNameVal = $item.Substring($dotIdx + 1)
|
$objNameVal = $item.Substring($dotIdx + 1)
|
||||||
|
|
||||||
# Check type is valid
|
# Check type is valid
|
||||||
@@ -439,11 +735,11 @@ To create a new $typeName, use $hintSkill (auto-registers in Configuration.xml):
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
# Find insertion point: after last element of same type, or after last element of preceding type
|
# Место вставки. Вид — по $script:typeOrder; внутри вида — по newObjectPosition.
|
||||||
|
$byName = (-not (Test-OrderSensitiveType $typeName) -and (Get-NewObjectPosition $script:configDir) -eq "byName")
|
||||||
$insertBefore = $null
|
$insertBefore = $null
|
||||||
$lastSameType = $null
|
$lastSameType = $null
|
||||||
$lastPrecedingType = $null
|
$firstLaterType = $null
|
||||||
$currentTypeIdx = -1
|
|
||||||
|
|
||||||
foreach ($child in $script:childObjsEl.ChildNodes) {
|
foreach ($child in $script:childObjsEl.ChildNodes) {
|
||||||
if ($child.NodeType -ne 'Element') { continue }
|
if ($child.NodeType -ne 'Element') { continue }
|
||||||
@@ -451,17 +747,29 @@ To create a new $typeName, use $hintSkill (auto-registers in Configuration.xml):
|
|||||||
if ($childTypeIdx -lt 0) { continue }
|
if ($childTypeIdx -lt 0) { continue }
|
||||||
|
|
||||||
if ($child.LocalName -eq $typeName) {
|
if ($child.LocalName -eq $typeName) {
|
||||||
# Same type — check alphabetical order
|
# Внутри вида — по newObjectPosition: end (по умолчанию) кладёт после последнего
|
||||||
if ($child.InnerText -gt $objNameVal -and -not $insertBefore) {
|
# объекта того же вида, byName — по имени. Subsystem по имени не упорядочиваем
|
||||||
# Insert before this element (alphabetical)
|
# никогда: порядок подсистем в дереве задаёт порядок разделов в панели.
|
||||||
|
$lastSameType = $child
|
||||||
|
if ($byName -and -not $insertBefore -and (Compare-MetadataNames $child.InnerText $objNameVal) -gt 0) {
|
||||||
$insertBefore = $child
|
$insertBefore = $child
|
||||||
}
|
}
|
||||||
$lastSameType = $child
|
} elseif ($childTypeIdx -gt $typeIdx -and -not $firstLaterType) {
|
||||||
} elseif ($childTypeIdx -lt $typeIdx) {
|
$firstLaterType = $child
|
||||||
$lastPrecedingType = $child
|
}
|
||||||
} elseif ($childTypeIdx -gt $typeIdx -and -not $insertBefore) {
|
}
|
||||||
# First element of a later type — insert before it
|
|
||||||
$insertBefore = $child
|
if (-not $insertBefore) {
|
||||||
|
# Место не выбрано именем — ставим сразу за последним объектом того же вида,
|
||||||
|
# то есть перед его следующим соседом. Через $firstLaterType этого не сделать:
|
||||||
|
# если видов старше в файле нет, запись уехала бы в самый конец блока,
|
||||||
|
# за пределы своей группы.
|
||||||
|
if ($lastSameType) {
|
||||||
|
$next = $lastSameType.NextSibling
|
||||||
|
while ($next -and $next.NodeType -ne 'Element') { $next = $next.NextSibling }
|
||||||
|
$insertBefore = $next
|
||||||
|
} else {
|
||||||
|
$insertBefore = $firstLaterType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -493,6 +801,8 @@ function Do-RemoveChildObject([string]$batchVal) {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
$typeName = $item.Substring(0, $dotIdx)
|
$typeName = $item.Substring(0, $dotIdx)
|
||||||
|
$canonType = Resolve-TypeName $typeName
|
||||||
|
if ($canonType) { $typeName = $canonType }
|
||||||
$objNameVal = $item.Substring($dotIdx + 1)
|
$objNameVal = $item.Substring($dotIdx + 1)
|
||||||
|
|
||||||
$found = $false
|
$found = $false
|
||||||
@@ -639,10 +949,7 @@ function Do-SetPanels($valArg) {
|
|||||||
# Accept string (JSON), PSCustomObject, or hashtable
|
# Accept string (JSON), PSCustomObject, or hashtable
|
||||||
$layout = $valArg
|
$layout = $valArg
|
||||||
if ($layout -is [string]) {
|
if ($layout -is [string]) {
|
||||||
try { $layout = $layout | ConvertFrom-Json } catch {
|
$layout = ConvertFrom-JsonInput $layout "-Value for operation 'set-panels'" "a JSON object with panel layout" -Inline
|
||||||
Write-Error "set-panels value must be valid JSON object, got: $valArg"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (-not $layout) {
|
if (-not $layout) {
|
||||||
Write-Error "set-panels value is empty"
|
Write-Error "set-panels value is empty"
|
||||||
@@ -725,6 +1032,29 @@ $script:ruTypeMap = @{
|
|||||||
"бот" = "Bot"
|
"бот" = "Bot"
|
||||||
"планобмена" = "ExchangePlan"
|
"планобмена" = "ExchangePlan"
|
||||||
"хранилищенастроек" = "SettingsStorage"
|
"хранилищенастроек" = "SettingsStorage"
|
||||||
|
# Множественное число: в дереве конфигурации виды подписаны именно так.
|
||||||
|
"справочники" = "Catalog"
|
||||||
|
"документы" = "Document"
|
||||||
|
"перечисления" = "Enum"
|
||||||
|
"отчёты" = "Report"
|
||||||
|
"отчеты" = "Report"
|
||||||
|
"обработки" = "DataProcessor"
|
||||||
|
"общиеформы" = "CommonForm"
|
||||||
|
"журналыдокументов" = "DocumentJournal"
|
||||||
|
"планывидовхарактеристик" = "ChartOfCharacteristicTypes"
|
||||||
|
"планысчетов" = "ChartOfAccounts"
|
||||||
|
"планывидоврасчета" = "ChartOfCalculationTypes"
|
||||||
|
"планывидоврасчёта" = "ChartOfCalculationTypes"
|
||||||
|
"регистрысведений" = "InformationRegister"
|
||||||
|
"регистрынакопления" = "AccumulationRegister"
|
||||||
|
"регистрыбухгалтерии" = "AccountingRegister"
|
||||||
|
"регистрырасчета" = "CalculationRegister"
|
||||||
|
"регистрырасчёта" = "CalculationRegister"
|
||||||
|
"бизнеспроцессы" = "BusinessProcess"
|
||||||
|
"задачи" = "Task"
|
||||||
|
"боты" = "Bot"
|
||||||
|
"планыобмена" = "ExchangePlan"
|
||||||
|
"хранилищанастроек" = "SettingsStorage"
|
||||||
}
|
}
|
||||||
# plural folder → singular type
|
# plural folder → singular type
|
||||||
$script:dirToType = @{}
|
$script:dirToType = @{}
|
||||||
@@ -826,9 +1156,7 @@ $indent</Item>
|
|||||||
function Do-SetHomePage($valArg) {
|
function Do-SetHomePage($valArg) {
|
||||||
$layout = $valArg
|
$layout = $valArg
|
||||||
if ($layout -is [string]) {
|
if ($layout -is [string]) {
|
||||||
try { $layout = $layout | ConvertFrom-Json } catch {
|
$layout = ConvertFrom-JsonInput $layout "-Value for operation 'set-home-page'" "a JSON object with home page layout" -Inline
|
||||||
Write-Error "set-home-page value must be valid JSON object"; exit 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (-not $layout) { Write-Error "set-home-page value is empty"; exit 1 }
|
if (-not $layout) { Write-Error "set-home-page value is empty"; exit 1 }
|
||||||
|
|
||||||
@@ -942,8 +1270,8 @@ if ($DefinitionFile) {
|
|||||||
if (-not [System.IO.Path]::IsPathRooted($DefinitionFile)) {
|
if (-not [System.IO.Path]::IsPathRooted($DefinitionFile)) {
|
||||||
$DefinitionFile = Join-Path (Get-Location).Path $DefinitionFile
|
$DefinitionFile = Join-Path (Get-Location).Path $DefinitionFile
|
||||||
}
|
}
|
||||||
$jsonText = Get-Content -Raw -Encoding UTF8 $DefinitionFile
|
$jsonText = Read-JsonInputFile $DefinitionFile
|
||||||
$ops = $jsonText | ConvertFrom-Json
|
$ops = ConvertFrom-JsonInput $jsonText $DefinitionFile
|
||||||
if ($ops -is [System.Array]) {
|
if ($ops -is [System.Array]) {
|
||||||
foreach ($op in $ops) { $operations += $op }
|
foreach ($op in $ops) { $operations += $op }
|
||||||
} else {
|
} else {
|
||||||
@@ -968,11 +1296,16 @@ foreach ($op in $operations) {
|
|||||||
"set-defaultRoles" { Do-SetDefaultRoles $opValueStr }
|
"set-defaultRoles" { Do-SetDefaultRoles $opValueStr }
|
||||||
"set-panels" { Do-SetPanels $opValue }
|
"set-panels" { Do-SetPanels $opValue }
|
||||||
"set-home-page" { Do-SetHomePage $opValue }
|
"set-home-page" { Do-SetHomePage $opValue }
|
||||||
|
"sort-childObjects" { Do-SortChildObjects $opValueStr }
|
||||||
default { Write-Error "Unknown operation: $opName"; exit 1 }
|
default { Write-Error "Unknown operation: $opName"; exit 1 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Save ---
|
# --- Save ---
|
||||||
|
# Стиль исходника снимаем ДО записи: правка чужого файла наследует его BOM/EOL/заголовок
|
||||||
|
# (#44/#46/#47), новый файл получает канон выгрузки. Зеркало _detect_xml_style в py-порту.
|
||||||
|
$xmlStyle = Detect-XmlStyle $resolvedPath
|
||||||
|
|
||||||
$settings = New-Object System.Xml.XmlWriterSettings
|
$settings = New-Object System.Xml.XmlWriterSettings
|
||||||
$settings.Encoding = New-Object System.Text.UTF8Encoding($true)
|
$settings.Encoding = New-Object System.Text.UTF8Encoding($true)
|
||||||
$settings.Indent = $false
|
$settings.Indent = $false
|
||||||
@@ -983,22 +1316,12 @@ $writer = [System.Xml.XmlWriter]::Create($memStream, $settings)
|
|||||||
$script:xmlDoc.Save($writer)
|
$script:xmlDoc.Save($writer)
|
||||||
$writer.Flush(); $writer.Close()
|
$writer.Flush(); $writer.Close()
|
||||||
|
|
||||||
$bytes = $memStream.ToArray()
|
$text = [System.Text.Encoding]::UTF8.GetString($memStream.ToArray())
|
||||||
$memStream.Close()
|
$memStream.Close()
|
||||||
$text = [System.Text.Encoding]::UTF8.GetString($bytes)
|
$text = Finalize-XmlText $text $xmlStyle
|
||||||
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
|
|
||||||
$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
|
||||||
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
|
||||||
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
|
||||||
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
|
||||||
$text = [regex]::Replace($text, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
|
||||||
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
|
||||||
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
|
||||||
$targetEol = if ((Test-Path -LiteralPath $resolvedPath) -and ([System.IO.File]::ReadAllText($resolvedPath) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
|
||||||
$text = ($text -replace "`r`n", "`n") -replace "`n", $targetEol
|
|
||||||
|
|
||||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
$writeBom = ($null -eq $xmlStyle) -or $xmlStyle.bom
|
||||||
[System.IO.File]::WriteAllText($resolvedPath, $text, $utf8Bom)
|
[System.IO.File]::WriteAllText($resolvedPath, $text, (New-Object System.Text.UTF8Encoding($writeBom)))
|
||||||
Info "Saved: $resolvedPath"
|
Info "Saved: $resolvedPath"
|
||||||
|
|
||||||
# --- Auto-validate ---
|
# --- Auto-validate ---
|
||||||
+297
-42
@@ -1,8 +1,9 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cf-edit v1.19 — Edit 1C configuration root (Configuration.xml)
|
# cf-edit v1.28 — Edit 1C configuration root (Configuration.xml)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import functools
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
@@ -14,6 +15,68 @@ from lxml import etree
|
|||||||
|
|
||||||
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
# регистр не различают, в argparse совпадение точное.
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
|
||||||
|
def parse_json_input(text, source, expected=None, inline=False):
|
||||||
|
"""Разбор пользовательского JSON: одна строка в stderr вместо traceback (issue #80).
|
||||||
|
|
||||||
|
expected заполняем только для полиморфного входа: у файла подсказка
|
||||||
|
была бы наполнителем — имя файла и текст парсера самодостаточны. inline печатает ещё и то,
|
||||||
|
что доехало: у файла такого вопроса нет, он лежит на диске и его видно целиком.
|
||||||
|
|
||||||
|
Импорты внутри тела: копия функции живёт в навыках с разными именами модулей
|
||||||
|
(skd-decompile импортирует json локально как _json), а тело обязано быть одинаковым.
|
||||||
|
"""
|
||||||
|
import json as _pj
|
||||||
|
import sys as _psys
|
||||||
|
try:
|
||||||
|
if not str(text).strip():
|
||||||
|
raise ValueError("input is empty")
|
||||||
|
return _pj.loads(text)
|
||||||
|
except ValueError as exc:
|
||||||
|
what = "%s expects %s" % (source, expected) if expected else "Invalid JSON in %s" % source
|
||||||
|
if inline:
|
||||||
|
got = " ".join(str(text).split())
|
||||||
|
label = "got"
|
||||||
|
if not got:
|
||||||
|
got = "(empty)"
|
||||||
|
elif len(got) > 60:
|
||||||
|
label = "got (first 60 chars)"
|
||||||
|
got = got[:60]
|
||||||
|
what = "%s, %s: %s" % (what, label, got)
|
||||||
|
print("[ERROR] %s (%s)" % (what, exc), file=_psys.stderr)
|
||||||
|
_psys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def read_json_file(path):
|
||||||
|
"""Чтение входного JSON-файла с кодировкой из BOM (issue #80).
|
||||||
|
|
||||||
|
BOM — объявление самого файла, поэтому ему верим; без BOM ждём строгий UTF-8. Кодовую
|
||||||
|
страницу не подбираем: угаданное имя уехало бы в метаданные молча.
|
||||||
|
"""
|
||||||
|
import os as _pos
|
||||||
|
import sys as _psys
|
||||||
|
if not _pos.path.exists(path):
|
||||||
|
print("[ERROR] File not found: %s" % path, file=_psys.stderr)
|
||||||
|
_psys.exit(1)
|
||||||
|
if _pos.path.isdir(path):
|
||||||
|
print("[ERROR] Expected a JSON file, got a directory: %s" % path, file=_psys.stderr)
|
||||||
|
_psys.exit(1)
|
||||||
|
with open(path, "rb") as _fh:
|
||||||
|
data = _fh.read()
|
||||||
|
if data[:3] == b"\xef\xbb\xbf":
|
||||||
|
return data[3:].decode("utf-8")
|
||||||
|
if data[:2] == b"\xff\xfe":
|
||||||
|
return data[2:].decode("utf-16-le")
|
||||||
|
if data[:2] == b"\xfe\xff":
|
||||||
|
return data[2:].decode("utf-16-be")
|
||||||
|
try:
|
||||||
|
return data.decode("utf-8")
|
||||||
|
except UnicodeDecodeError as exc:
|
||||||
|
print("[ERROR] %s is not valid UTF-8: %s - save the file as UTF-8, or add a BOM if it is UTF-16"
|
||||||
|
% (path, exc), file=_psys.stderr)
|
||||||
|
_psys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
class CIDict(dict):
|
class CIDict(dict):
|
||||||
# Ключи храним КАК ЕСТЬ: часть из них — имена объектов (табличные части, стандартные
|
# Ключи храним КАК ЕСТЬ: часть из них — имена объектов (табличные части, стандартные
|
||||||
# реквизиты), они попадают в XML. Регистронезависим только поиск. Порядок вставки
|
# реквизиты), они попадают в XML. Регистронезависим только поиск. Порядок вставки
|
||||||
@@ -254,14 +317,14 @@ XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
|
|||||||
V8_NS = "http://v8.1c.ru/8.1/data/core"
|
V8_NS = "http://v8.1c.ru/8.1/data/core"
|
||||||
XS_NS = "http://www.w3.org/2001/XMLSchema"
|
XS_NS = "http://www.w3.org/2001/XMLSchema"
|
||||||
|
|
||||||
# Canonical type order for ChildObjects (44 types)
|
# Canonical type order for ChildObjects (46 types)
|
||||||
TYPE_ORDER = [
|
TYPE_ORDER = [
|
||||||
"Language", "Subsystem", "StyleItem", "Style",
|
"Language", "Subsystem", "StyleItem", "Style",
|
||||||
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
|
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
|
||||||
"FilterCriterion", "CommonModule", "Bot", "CommonAttribute", "ExchangePlan",
|
"FilterCriterion", "CommonModule", "CommonAttribute", "ExchangePlan",
|
||||||
"XDTOPackage", "WebService", "HTTPService", "WSReference",
|
"XDTOPackage", "WebService", "HTTPService", "WSReference",
|
||||||
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
|
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
|
||||||
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
|
"FunctionalOptionsParameter", "DefinedType", "Bot", "PaletteColor", "CommonCommand", "CommandGroup",
|
||||||
"Constant", "CommonForm", "Catalog", "Document",
|
"Constant", "CommonForm", "Catalog", "Document",
|
||||||
"DocumentNumerator", "Sequence", "DocumentJournal", "Enum",
|
"DocumentNumerator", "Sequence", "DocumentJournal", "Enum",
|
||||||
"Report", "DataProcessor", "InformationRegister", "AccumulationRegister",
|
"Report", "DataProcessor", "InformationRegister", "AccumulationRegister",
|
||||||
@@ -274,7 +337,7 @@ TYPE_ORDER = [
|
|||||||
TYPE_TO_DIR = {
|
TYPE_TO_DIR = {
|
||||||
"Language": "Languages", "Subsystem": "Subsystems", "StyleItem": "StyleItems", "Style": "Styles",
|
"Language": "Languages", "Subsystem": "Subsystems", "StyleItem": "StyleItems", "Style": "Styles",
|
||||||
"CommonPicture": "CommonPictures", "SessionParameter": "SessionParameters", "Role": "Roles", "CommonTemplate": "CommonTemplates",
|
"CommonPicture": "CommonPictures", "SessionParameter": "SessionParameters", "Role": "Roles", "CommonTemplate": "CommonTemplates",
|
||||||
"FilterCriterion": "FilterCriteria", "CommonModule": "CommonModules", "Bot": "Bots", "CommonAttribute": "CommonAttributes", "ExchangePlan": "ExchangePlans",
|
"FilterCriterion": "FilterCriteria", "CommonModule": "CommonModules", "Bot": "Bots", "PaletteColor": "PaletteColors", "PaletteColor": "PaletteColors", "CommonAttribute": "CommonAttributes", "ExchangePlan": "ExchangePlans",
|
||||||
"XDTOPackage": "XDTOPackages", "WebService": "WebServices", "HTTPService": "HTTPServices", "WSReference": "WSReferences",
|
"XDTOPackage": "XDTOPackages", "WebService": "WebServices", "HTTPService": "HTTPServices", "WSReference": "WSReferences",
|
||||||
"EventSubscription": "EventSubscriptions", "ScheduledJob": "ScheduledJobs", "SettingsStorage": "SettingsStorages", "FunctionalOption": "FunctionalOptions",
|
"EventSubscription": "EventSubscriptions", "ScheduledJob": "ScheduledJobs", "SettingsStorage": "SettingsStorages", "FunctionalOption": "FunctionalOptions",
|
||||||
"FunctionalOptionsParameter": "FunctionalOptionsParameters", "DefinedType": "DefinedTypes", "CommonCommand": "CommonCommands", "CommandGroup": "CommandGroups",
|
"FunctionalOptionsParameter": "FunctionalOptionsParameters", "DefinedType": "DefinedTypes", "CommonCommand": "CommonCommands", "CommandGroup": "CommandGroups",
|
||||||
@@ -291,6 +354,137 @@ SCALAR_PROPS = ["Name", "Version", "Vendor", "Comment", "NamePrefix", "UpdateCat
|
|||||||
REF_PROPS = ["DefaultLanguage"]
|
REF_PROPS = ["DefaultLanguage"]
|
||||||
|
|
||||||
|
|
||||||
|
def get_new_object_position(cfg_dir):
|
||||||
|
"""Куда навык ставит новую запись в <ChildObjects> — настройка newObjectPosition.
|
||||||
|
|
||||||
|
databases[].newObjectPosition базы, чей configSrc охватывает каталог родительского XML,
|
||||||
|
иначе корневое поле, иначе end. Значения: end — после последнего объекта того же вида
|
||||||
|
(так дописывает Конфигуратор); byName — по имени среди объектов того же вида.
|
||||||
|
Файл ищем от рабочего каталога вверх, каталог конфигурации — запасной путь: так же
|
||||||
|
его ищут support-guard и группа db-*, а скрипт навыка зовут по абсолютному пути, и cwd
|
||||||
|
остаётся рабочим каталогом проекта.
|
||||||
|
configSrc считается от каталога .v8-project.json, как задокументировано в
|
||||||
|
docs/v8-project-guide.md. Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(os.path.abspath(cfg_dir or "."))
|
||||||
|
if not pj:
|
||||||
|
return "end"
|
||||||
|
proj = json.loads(open(pj, encoding="utf-8-sig").read())
|
||||||
|
proj_dir = os.path.dirname(pj)
|
||||||
|
cfg_full = os.path.normcase(os.path.abspath(cfg_dir or ".")).rstrip("\\/")
|
||||||
|
for db in proj.get("databases", []):
|
||||||
|
src = db.get("configSrc")
|
||||||
|
if src and db.get("newObjectPosition"):
|
||||||
|
src_full = os.path.normcase(os.path.abspath(os.path.join(proj_dir, src))).rstrip("\\/")
|
||||||
|
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
|
||||||
|
return "byName" if str(db["newObjectPosition"]).lower() == "byname" else "end"
|
||||||
|
if str(proj.get("newObjectPosition") or "").lower() == "byname":
|
||||||
|
return "byName"
|
||||||
|
return "end"
|
||||||
|
except Exception:
|
||||||
|
return "end"
|
||||||
|
|
||||||
|
|
||||||
|
def is_order_sensitive_type(type_name):
|
||||||
|
"""Виды, у которых порядок в дереве несёт смысл: автоматически их не упорядочиваем.
|
||||||
|
|
||||||
|
CommonAttribute — исключение самого стандарта (#std467): у общих реквизитов-разделителей
|
||||||
|
порядок в дереве задаёт порядок установки параметров сеанса. Subsystem и CommandGroup:
|
||||||
|
пока они не перечислены в <SubsystemsOrder> / <GroupsOrder> файла Ext/CommandInterface.xml,
|
||||||
|
порядок дерева задаёт порядок в интерфейсе, а платформа эти списки сама не заводит
|
||||||
|
(в выгрузке ACC вне GroupsOrder 15 живых групп из 39). Language исключён из осторожности,
|
||||||
|
без замера: языков обычно один-два, и в типовых их порядок не алфавитный.
|
||||||
|
Явно названный вид сортируется в любом случае.
|
||||||
|
Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||||
|
"""
|
||||||
|
return type_name in ("CommonAttribute", "Subsystem", "CommandGroup", "Language")
|
||||||
|
|
||||||
|
|
||||||
|
def compare_metadata_names(a, b):
|
||||||
|
"""Порядок имён объектов метаданных, как в дереве Конфигуратора.
|
||||||
|
|
||||||
|
Ключ — пары «ранг+символ»: регистр не учитывается, подчёркивание раньше цифр, цифры раньше
|
||||||
|
букв, буквы по кодам (латиница раньше кириллицы), ё на месте е. Культурные таблицы не
|
||||||
|
используются — они разные на разных ОС и в разных рантаймах, а так оба порта сравнивают
|
||||||
|
одинаково везде. Равные ключи разводит ordinal-сравнение исходных строк.
|
||||||
|
Возвращает -1 | 0 | 1. Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||||
|
"""
|
||||||
|
keys = []
|
||||||
|
for name in (a, b):
|
||||||
|
parts = []
|
||||||
|
for ch in name.lower():
|
||||||
|
if ch == "ё":
|
||||||
|
ch = "е"
|
||||||
|
if ch.isdigit():
|
||||||
|
parts.append("1" + ch)
|
||||||
|
elif ch.isalpha():
|
||||||
|
parts.append("2" + ch)
|
||||||
|
else:
|
||||||
|
parts.append("0" + ch)
|
||||||
|
keys.append("".join(parts))
|
||||||
|
if keys[0] != keys[1]:
|
||||||
|
return -1 if keys[0] < keys[1] else 1
|
||||||
|
if a != b:
|
||||||
|
return -1 if a < b else 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
RU_TYPE_MAP = {
|
||||||
|
"справочник": "Catalog", "документ": "Document", "перечисление": "Enum",
|
||||||
|
"отчёт": "Report", "отчет": "Report", "обработка": "DataProcessor",
|
||||||
|
"общаяформа": "CommonForm", "журналдокументов": "DocumentJournal",
|
||||||
|
"планвидовхарактеристик": "ChartOfCharacteristicTypes",
|
||||||
|
"плансчетов": "ChartOfAccounts",
|
||||||
|
"планвидоврасчета": "ChartOfCalculationTypes",
|
||||||
|
"планвидоврасчёта": "ChartOfCalculationTypes",
|
||||||
|
"регистрсведений": "InformationRegister",
|
||||||
|
"регистрнакопления": "AccumulationRegister",
|
||||||
|
"регистрбухгалтерии": "AccountingRegister",
|
||||||
|
"регистррасчета": "CalculationRegister",
|
||||||
|
"регистррасчёта": "CalculationRegister",
|
||||||
|
"бизнеспроцесс": "BusinessProcess",
|
||||||
|
"бот": "Bot",
|
||||||
|
"задача": "Task", "планобмена": "ExchangePlan",
|
||||||
|
"хранилищенастроек": "SettingsStorage",
|
||||||
|
# Множественное число: в дереве конфигурации виды подписаны именно так.
|
||||||
|
"справочники": "Catalog", "документы": "Document", "перечисления": "Enum",
|
||||||
|
"отчёты": "Report", "отчеты": "Report", "обработки": "DataProcessor",
|
||||||
|
"общиеформы": "CommonForm", "журналыдокументов": "DocumentJournal",
|
||||||
|
"планывидовхарактеристик": "ChartOfCharacteristicTypes",
|
||||||
|
"планысчетов": "ChartOfAccounts",
|
||||||
|
"планывидоврасчета": "ChartOfCalculationTypes",
|
||||||
|
"планывидоврасчёта": "ChartOfCalculationTypes",
|
||||||
|
"регистрысведений": "InformationRegister",
|
||||||
|
"регистрынакопления": "AccumulationRegister",
|
||||||
|
"регистрыбухгалтерии": "AccountingRegister",
|
||||||
|
"регистррасчета": "CalculationRegister", "регистрырасчета": "CalculationRegister",
|
||||||
|
"регистрырасчёта": "CalculationRegister",
|
||||||
|
"бизнеспроцессы": "BusinessProcess",
|
||||||
|
"боты": "Bot",
|
||||||
|
"задачи": "Task", "планыобмена": "ExchangePlan",
|
||||||
|
"хранилищанастроек": "SettingsStorage",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_type_name(token):
|
||||||
|
"""Имя вида из пользовательского ввода → каноническое имя или None.
|
||||||
|
|
||||||
|
Ввод прощающий: регистр не важен, принимается имя каталога выгрузки
|
||||||
|
(Catalogs → Catalog) и русское имя вида в единственном и множественном числе.
|
||||||
|
"""
|
||||||
|
key = (token or "").strip().lower()
|
||||||
|
if not key:
|
||||||
|
return None
|
||||||
|
for canon in TYPE_ORDER:
|
||||||
|
if canon.lower() == key:
|
||||||
|
return canon
|
||||||
|
for canon, dir_name in TYPE_TO_DIR.items():
|
||||||
|
if dir_name.lower() == key:
|
||||||
|
return canon
|
||||||
|
return RU_TYPE_MAP.get(key)
|
||||||
|
|
||||||
|
|
||||||
def localname(el):
|
def localname(el):
|
||||||
return etree.QName(el.tag).localname
|
return etree.QName(el.tag).localname
|
||||||
|
|
||||||
@@ -436,7 +630,7 @@ def main():
|
|||||||
parser = argparse.ArgumentParser(description="Edit 1C configuration root (Configuration.xml)", allow_abbrev=False)
|
parser = argparse.ArgumentParser(description="Edit 1C configuration root (Configuration.xml)", allow_abbrev=False)
|
||||||
parser.add_argument("-ConfigPath", "-Path", required=True)
|
parser.add_argument("-ConfigPath", "-Path", required=True)
|
||||||
parser.add_argument("-DefinitionFile", default=None)
|
parser.add_argument("-DefinitionFile", default=None)
|
||||||
parser.add_argument("-Operation", default=None, choices=["modify-property", "add-childObject", "remove-childObject", "add-defaultRole", "remove-defaultRole", "set-defaultRoles", "set-panels", "set-home-page"])
|
parser.add_argument("-Operation", default=None, choices=["modify-property", "add-childObject", "remove-childObject", "add-defaultRole", "remove-defaultRole", "set-defaultRoles", "set-panels", "set-home-page", "sort-childObjects"])
|
||||||
parser.add_argument("-Value", default=None)
|
parser.add_argument("-Value", default=None)
|
||||||
parser.add_argument("-NoValidate", action="store_true")
|
parser.add_argument("-NoValidate", action="store_true")
|
||||||
args = ci_parse_args(parser)
|
args = ci_parse_args(parser)
|
||||||
@@ -575,7 +769,7 @@ def main():
|
|||||||
if dot_idx < 1:
|
if dot_idx < 1:
|
||||||
print(f"Invalid format '{item}', expected 'Type.Name'", file=sys.stderr)
|
print(f"Invalid format '{item}', expected 'Type.Name'", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
type_name = item[:dot_idx]
|
type_name = resolve_type_name(item[:dot_idx]) or item[:dot_idx]
|
||||||
obj_name_val = item[dot_idx + 1:]
|
obj_name_val = item[dot_idx + 1:]
|
||||||
|
|
||||||
if type_name not in TYPE_ORDER:
|
if type_name not in TYPE_ORDER:
|
||||||
@@ -612,8 +806,15 @@ def main():
|
|||||||
warn(f"Already exists: {type_name}.{obj_name_val}")
|
warn(f"Already exists: {type_name}.{obj_name_val}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Find insertion point
|
# Место вставки. Вид — по TYPE_ORDER; внутри вида — по newObjectPosition:
|
||||||
|
# end (по умолчанию) кладёт после последнего объекта того же вида, byName — по имени.
|
||||||
|
# Subsystem по имени не упорядочиваем никогда: порядок подсистем в дереве задаёт
|
||||||
|
# порядок разделов в панели, пока их не перечислили в <SubsystemsOrder>.
|
||||||
|
by_name = (not is_order_sensitive_type(type_name)
|
||||||
|
and get_new_object_position(config_dir) == "byName")
|
||||||
insert_before = None
|
insert_before = None
|
||||||
|
last_same = None
|
||||||
|
first_later = None
|
||||||
for child in child_objs_el:
|
for child in child_objs_el:
|
||||||
if not isinstance(child.tag, str):
|
if not isinstance(child.tag, str):
|
||||||
continue
|
continue
|
||||||
@@ -623,10 +824,24 @@ def main():
|
|||||||
child_type_idx = TYPE_ORDER.index(child_type_name)
|
child_type_idx = TYPE_ORDER.index(child_type_name)
|
||||||
|
|
||||||
if child_type_name == type_name:
|
if child_type_name == type_name:
|
||||||
if (child.text or "") > obj_name_val and insert_before is None:
|
last_same = child
|
||||||
|
if (by_name and insert_before is None
|
||||||
|
and compare_metadata_names(child.text or "", obj_name_val) > 0):
|
||||||
insert_before = child
|
insert_before = child
|
||||||
elif child_type_idx > type_idx and insert_before is None:
|
elif child_type_idx > type_idx and first_later is None:
|
||||||
insert_before = child
|
first_later = child
|
||||||
|
|
||||||
|
if insert_before is None:
|
||||||
|
# Место не выбрано именем — ставим сразу за последним объектом того же вида,
|
||||||
|
# то есть перед его следующим соседом. Через first_later этого не сделать:
|
||||||
|
# если видов старше в файле нет, запись уехала бы в самый конец блока,
|
||||||
|
# за пределы своей группы.
|
||||||
|
if last_same is not None:
|
||||||
|
siblings = [c for c in child_objs_el if isinstance(c.tag, str)]
|
||||||
|
pos = siblings.index(last_same)
|
||||||
|
insert_before = siblings[pos + 1] if pos + 1 < len(siblings) else None
|
||||||
|
else:
|
||||||
|
insert_before = first_later
|
||||||
|
|
||||||
new_el = etree.Element(f"{{{MD_NS}}}{type_name}")
|
new_el = etree.Element(f"{{{MD_NS}}}{type_name}")
|
||||||
new_el.text = obj_name_val
|
new_el.text = obj_name_val
|
||||||
@@ -639,6 +854,69 @@ def main():
|
|||||||
add_count += 1
|
add_count += 1
|
||||||
info(f"Added: {type_name}.{obj_name_val}")
|
info(f"Added: {type_name}.{obj_name_val}")
|
||||||
|
|
||||||
|
def do_sort_child_objects(batch_val):
|
||||||
|
"""Упорядочить <ChildObjects>: имена внутри вида, а без аргумента — и группы видов.
|
||||||
|
|
||||||
|
Виды из is_order_sensitive_type по имени не сортируются, пока не названы явно.
|
||||||
|
Вызов без значения дополнительно ставит группы видов в канонический порядок: платформа
|
||||||
|
починила бы его только при загрузке-выгрузке, то есть неканоничный файл даёт диф на
|
||||||
|
ровном месте. Переставляем ЗНАЧЕНИЯ узлов, а не сами узлы — отступы и структура файла
|
||||||
|
остаются как были, в дифе только перестановка строк.
|
||||||
|
"""
|
||||||
|
nonlocal modify_count
|
||||||
|
if child_objs_el is None:
|
||||||
|
print("No <ChildObjects> element found", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
requested = []
|
||||||
|
for token in (parse_batch_value(batch_val) if str(batch_val or "").strip() else []):
|
||||||
|
canon = resolve_type_name(token)
|
||||||
|
if canon is None:
|
||||||
|
print(f"Unknown type '{token}'. Valid: {', '.join(TYPE_ORDER)}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
requested.append(canon)
|
||||||
|
|
||||||
|
groups = {}
|
||||||
|
for child in child_objs_el:
|
||||||
|
if not isinstance(child.tag, str):
|
||||||
|
continue
|
||||||
|
groups.setdefault(localname(child), []).append(child)
|
||||||
|
|
||||||
|
targets = requested or [t for t in groups if not is_order_sensitive_type(t)]
|
||||||
|
for type_name in targets:
|
||||||
|
els = groups.get(type_name, [])
|
||||||
|
if len(els) < 2:
|
||||||
|
continue
|
||||||
|
names = [e.text or "" for e in els]
|
||||||
|
ordered = sorted(names, key=functools.cmp_to_key(compare_metadata_names))
|
||||||
|
if names == ordered:
|
||||||
|
continue
|
||||||
|
for el, name in zip(els, ordered):
|
||||||
|
el.text = name
|
||||||
|
modify_count += 1
|
||||||
|
info(f"Sorted: {type_name} ({len(els)})")
|
||||||
|
|
||||||
|
if requested:
|
||||||
|
# Вид назван явно — точечная операция: взаимный порядок групп не трогаем.
|
||||||
|
return
|
||||||
|
|
||||||
|
# Без аргумента приводим в порядок и сами группы видов: собранная навыками
|
||||||
|
# конфигурация может держать их не в каноне, и первая же выгрузка платформы даст
|
||||||
|
# диф. Переставляем содержимое существующих узлов, а не узлы, поэтому отступы и
|
||||||
|
# структура файла не меняются — в дифе только перестановка строк.
|
||||||
|
elems = [c for c in child_objs_el if isinstance(c.tag, str)]
|
||||||
|
pairs = [(localname(c), c.text or "") for c in elems]
|
||||||
|
ranked = sorted(range(len(pairs)),
|
||||||
|
key=lambda i: (TYPE_ORDER.index(pairs[i][0]) if pairs[i][0] in TYPE_ORDER else len(TYPE_ORDER), i))
|
||||||
|
wanted = [pairs[i] for i in ranked]
|
||||||
|
if wanted == pairs:
|
||||||
|
return
|
||||||
|
for el, (tag, text) in zip(elems, wanted):
|
||||||
|
el.tag = f'{{{MD_NS}}}{tag}'
|
||||||
|
el.text = text
|
||||||
|
modify_count += 1
|
||||||
|
info(f"Reordered type groups: {len(elems)} entries")
|
||||||
|
|
||||||
def do_remove_child_object(batch_val):
|
def do_remove_child_object(batch_val):
|
||||||
nonlocal remove_count
|
nonlocal remove_count
|
||||||
if child_objs_el is None:
|
if child_objs_el is None:
|
||||||
@@ -651,7 +929,7 @@ def main():
|
|||||||
if dot_idx < 1:
|
if dot_idx < 1:
|
||||||
print(f"Invalid format '{item}', expected 'Type.Name'", file=sys.stderr)
|
print(f"Invalid format '{item}', expected 'Type.Name'", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
type_name = item[:dot_idx]
|
type_name = resolve_type_name(item[:dot_idx]) or item[:dot_idx]
|
||||||
obj_name_val = item[dot_idx + 1:]
|
obj_name_val = item[dot_idx + 1:]
|
||||||
|
|
||||||
found = False
|
found = False
|
||||||
@@ -821,11 +1099,8 @@ def main():
|
|||||||
nonlocal modify_count
|
nonlocal modify_count
|
||||||
layout = value
|
layout = value
|
||||||
if isinstance(layout, str):
|
if isinstance(layout, str):
|
||||||
try:
|
layout = ci_json(parse_json_input(
|
||||||
layout = ci_json(json.loads(layout))
|
layout, "-Value for operation 'set-panels'", "a JSON object with panel layout", inline=True))
|
||||||
except json.JSONDecodeError:
|
|
||||||
print(f"set-panels value must be valid JSON object", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
if not isinstance(layout, dict) or not layout:
|
if not isinstance(layout, dict) or not layout:
|
||||||
print("set-panels value must be non-empty object", file=sys.stderr)
|
print("set-panels value must be non-empty object", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@@ -874,24 +1149,6 @@ def main():
|
|||||||
info(f"Wrote panel layout: {cai_path}")
|
info(f"Wrote panel layout: {cai_path}")
|
||||||
|
|
||||||
# --- set-home-page (writes Ext/HomePageWorkArea.xml from scratch) ---
|
# --- set-home-page (writes Ext/HomePageWorkArea.xml from scratch) ---
|
||||||
RU_TYPE_MAP = {
|
|
||||||
"справочник": "Catalog", "документ": "Document", "перечисление": "Enum",
|
|
||||||
"отчёт": "Report", "отчет": "Report", "обработка": "DataProcessor",
|
|
||||||
"общаяформа": "CommonForm", "журналдокументов": "DocumentJournal",
|
|
||||||
"планвидовхарактеристик": "ChartOfCharacteristicTypes",
|
|
||||||
"плансчетов": "ChartOfAccounts",
|
|
||||||
"планвидоврасчета": "ChartOfCalculationTypes",
|
|
||||||
"планвидоврасчёта": "ChartOfCalculationTypes",
|
|
||||||
"регистрсведений": "InformationRegister",
|
|
||||||
"регистрнакопления": "AccumulationRegister",
|
|
||||||
"регистрбухгалтерии": "AccountingRegister",
|
|
||||||
"регистррасчета": "CalculationRegister",
|
|
||||||
"регистррасчёта": "CalculationRegister",
|
|
||||||
"бизнеспроцесс": "BusinessProcess",
|
|
||||||
"бот": "Bot",
|
|
||||||
"задача": "Task", "планобмена": "ExchangePlan",
|
|
||||||
"хранилищенастроек": "SettingsStorage",
|
|
||||||
}
|
|
||||||
DIR_TO_TYPE = {v.lower(): k for k, v in TYPE_TO_DIR.items()}
|
DIR_TO_TYPE = {v.lower(): k for k, v in TYPE_TO_DIR.items()}
|
||||||
UUID_RE = __import__("re").compile(r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")
|
UUID_RE = __import__("re").compile(r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")
|
||||||
|
|
||||||
@@ -976,11 +1233,8 @@ def main():
|
|||||||
nonlocal modify_count
|
nonlocal modify_count
|
||||||
layout = value
|
layout = value
|
||||||
if isinstance(layout, str):
|
if isinstance(layout, str):
|
||||||
try:
|
layout = ci_json(parse_json_input(
|
||||||
layout = ci_json(json.loads(layout))
|
layout, "-Value for operation 'set-home-page'", "a JSON object with home page layout", inline=True))
|
||||||
except json.JSONDecodeError:
|
|
||||||
print("set-home-page value must be valid JSON object", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
if not isinstance(layout, dict) or not layout:
|
if not isinstance(layout, dict) or not layout:
|
||||||
print("set-home-page value must be non-empty object", file=sys.stderr)
|
print("set-home-page value must be non-empty object", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@@ -1044,8 +1298,7 @@ def main():
|
|||||||
def_file = args.DefinitionFile
|
def_file = args.DefinitionFile
|
||||||
if not os.path.isabs(def_file):
|
if not os.path.isabs(def_file):
|
||||||
def_file = os.path.join(os.getcwd(), def_file)
|
def_file = os.path.join(os.getcwd(), def_file)
|
||||||
with open(def_file, "r", encoding="utf-8-sig") as fh:
|
ops = ci_json(parse_json_input(read_json_file(def_file), def_file))
|
||||||
ops = ci_json(json.loads(fh.read()))
|
|
||||||
if isinstance(ops, list):
|
if isinstance(ops, list):
|
||||||
operations = ops
|
operations = ops
|
||||||
else:
|
else:
|
||||||
@@ -1075,6 +1328,8 @@ def main():
|
|||||||
do_set_panels(op_value)
|
do_set_panels(op_value)
|
||||||
elif op_key == "set-home-page":
|
elif op_key == "set-home-page":
|
||||||
do_set_home_page(op_value)
|
do_set_home_page(op_value)
|
||||||
|
elif op_key == "sort-childobjects":
|
||||||
|
do_sort_child_objects(op_value if isinstance(op_value, str) else str(op_value))
|
||||||
else:
|
else:
|
||||||
print(f"Unknown operation: {op_name}", file=sys.stderr)
|
print(f"Unknown operation: {op_name}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@@ -23,7 +23,7 @@ allowed-tools:
|
|||||||
| `OutFile` | Записать результат в файл (UTF-8 BOM) |
|
| `OutFile` | Записать результат в файл (UTF-8 BOM) |
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-info.ps1" -ConfigPath "<путь>"
|
python ".gemini/skills/cf-info/scripts/cf-info.py" -ConfigPath "<путь>"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Три режима
|
## Три режима
|
||||||
+6
-5
@@ -1,7 +1,8 @@
|
|||||||
# cf-info v1.5 — Compact summary of 1C configuration root
|
# cf-info v1.8 — Compact summary of 1C configuration root
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
[CmdletBinding(PositionalBinding=$false)]
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory=$true)][Alias('Path')][string]$ConfigPath,
|
[Parameter(Mandatory=$true, Position=0)][Alias('Path')][string]$ConfigPath,
|
||||||
[ValidateSet("overview","brief","full")]
|
[ValidateSet("overview","brief","full")]
|
||||||
[string]$Mode = "overview",
|
[string]$Mode = "overview",
|
||||||
[Alias('Name')]
|
[Alias('Name')]
|
||||||
@@ -85,14 +86,14 @@ function Get-PropML([string]$propName) {
|
|||||||
return (Get-MLText $n)
|
return (Get-MLText $n)
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Type name maps (canonical order, 44 types) ---
|
# --- Type name maps (canonical order, 46 types) ---
|
||||||
$typeOrder = @(
|
$typeOrder = @(
|
||||||
"Language","Subsystem","StyleItem","Style",
|
"Language","Subsystem","StyleItem","Style",
|
||||||
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
||||||
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
|
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
|
||||||
"XDTOPackage","WebService","HTTPService","WSReference",
|
"XDTOPackage","WebService","HTTPService","WSReference",
|
||||||
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
||||||
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
"FunctionalOptionsParameter","DefinedType","Bot","PaletteColor","CommonCommand","CommandGroup",
|
||||||
"Constant","CommonForm","Catalog","Document",
|
"Constant","CommonForm","Catalog","Document",
|
||||||
"DocumentNumerator","Sequence","DocumentJournal","Enum",
|
"DocumentNumerator","Sequence","DocumentJournal","Enum",
|
||||||
"Report","DataProcessor","InformationRegister","AccumulationRegister",
|
"Report","DataProcessor","InformationRegister","AccumulationRegister",
|
||||||
+8
-8
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cf-info v1.5 — Compact summary of 1C configuration root
|
# cf-info v1.8 — Compact summary of 1C configuration root
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -61,11 +61,11 @@ if os.path.isdir(config_path):
|
|||||||
if os.path.isfile(candidate):
|
if os.path.isfile(candidate):
|
||||||
config_path = candidate
|
config_path = candidate
|
||||||
else:
|
else:
|
||||||
print(f"[ERROR] No Configuration.xml found in directory: {config_path}", file=sys.stderr)
|
print(f"[ERROR] No Configuration.xml found in directory: {config_path}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if not os.path.isfile(config_path):
|
if not os.path.isfile(config_path):
|
||||||
print(f"[ERROR] File not found: {config_path}", file=sys.stderr)
|
print(f"[ERROR] File not found: {config_path}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# --- Load XML ---
|
# --- Load XML ---
|
||||||
@@ -82,12 +82,12 @@ NS = {
|
|||||||
|
|
||||||
md_root = xml_root # root is MetaDataObject itself
|
md_root = xml_root # root is MetaDataObject itself
|
||||||
if etree.QName(md_root.tag).localname != "MetaDataObject":
|
if etree.QName(md_root.tag).localname != "MetaDataObject":
|
||||||
print("[ERROR] Not a valid 1C metadata XML file (no MetaDataObject root)", file=sys.stderr)
|
print("[ERROR] Not a valid 1C metadata XML file (no MetaDataObject root)")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
cfg_node = md_root.find("md:Configuration", NS)
|
cfg_node = md_root.find("md:Configuration", NS)
|
||||||
if cfg_node is None:
|
if cfg_node is None:
|
||||||
print("[ERROR] No <Configuration> element found", file=sys.stderr)
|
print("[ERROR] No <Configuration> element found")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
version = md_root.get("version", "")
|
version = md_root.get("version", "")
|
||||||
@@ -113,14 +113,14 @@ def get_prop_ml(prop_name):
|
|||||||
n = props_node.find(f"md:{prop_name}", NS)
|
n = props_node.find(f"md:{prop_name}", NS)
|
||||||
return get_ml_text(n)
|
return get_ml_text(n)
|
||||||
|
|
||||||
# --- Type name maps (canonical order, 44 types) ---
|
# --- Type name maps (canonical order, 46 types) ---
|
||||||
type_order = [
|
type_order = [
|
||||||
"Language", "Subsystem", "StyleItem", "Style",
|
"Language", "Subsystem", "StyleItem", "Style",
|
||||||
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
|
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
|
||||||
"FilterCriterion", "CommonModule", "Bot", "CommonAttribute", "ExchangePlan",
|
"FilterCriterion", "CommonModule", "CommonAttribute", "ExchangePlan",
|
||||||
"XDTOPackage", "WebService", "HTTPService", "WSReference",
|
"XDTOPackage", "WebService", "HTTPService", "WSReference",
|
||||||
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
|
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
|
||||||
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
|
"FunctionalOptionsParameter", "DefinedType", "Bot", "PaletteColor", "CommonCommand", "CommandGroup",
|
||||||
"Constant", "CommonForm", "Catalog", "Document",
|
"Constant", "CommonForm", "Catalog", "Document",
|
||||||
"DocumentNumerator", "Sequence", "DocumentJournal", "Enum",
|
"DocumentNumerator", "Sequence", "DocumentJournal", "Enum",
|
||||||
"Report", "DataProcessor", "InformationRegister", "AccumulationRegister",
|
"Report", "DataProcessor", "InformationRegister", "AccumulationRegister",
|
||||||
@@ -39,7 +39,7 @@ allowed-tools:
|
|||||||
не будет.
|
не будет.
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-init.ps1" -Name "МояКонфигурация"
|
python ".gemini/skills/cf-init/scripts/cf-init.py" -Name "МояКонфигурация"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
+2
-1
@@ -1,5 +1,6 @@
|
|||||||
# cf-init v1.14 — Create empty 1C configuration scaffold (+write_xml_file/write_utf8_bom: общий эталон записи)
|
# cf-init v1.15 — Create empty 1C configuration scaffold (+write_xml_file/write_utf8_bom: общий эталон записи)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
[CmdletBinding(PositionalBinding=$false)]
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
[string]$Name,
|
[string]$Name,
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cf-init v1.14 — Create empty 1C configuration scaffold (+write_xml_file/write_utf8_bom: общий эталон записи)
|
# cf-init v1.15 — Create empty 1C configuration scaffold (+write_xml_file/write_utf8_bom: общий эталон записи)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
"""Generates minimal XML source files for a 1C configuration."""
|
"""Generates minimal XML source files for a 1C configuration."""
|
||||||
import sys, os, argparse, re, uuid
|
import sys, os, argparse, re, uuid
|
||||||
@@ -24,6 +24,6 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-validate.ps1" -ConfigPath "upload/cfempty"
|
python ".gemini/skills/cf-validate/scripts/cf-validate.py" -ConfigPath "upload/cfempty"
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-validate.ps1" -ConfigPath "upload/cfempty/Configuration.xml"
|
python ".gemini/skills/cf-validate/scripts/cf-validate.py" -ConfigPath "upload/cfempty/Configuration.xml"
|
||||||
```
|
```
|
||||||
+6
-4
@@ -1,7 +1,8 @@
|
|||||||
# cf-validate v1.7 — Validate 1C configuration root structure
|
# cf-validate v1.9 — Validate 1C configuration root structure
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
[CmdletBinding(PositionalBinding=$false)]
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory, Position=0)]
|
||||||
[Alias('Path')]
|
[Alias('Path')]
|
||||||
[string]$ConfigPath,
|
[string]$ConfigPath,
|
||||||
|
|
||||||
@@ -121,10 +122,10 @@ $validClassIds = @(
|
|||||||
$childObjectTypes = @(
|
$childObjectTypes = @(
|
||||||
"Language","Subsystem","StyleItem","Style",
|
"Language","Subsystem","StyleItem","Style",
|
||||||
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
||||||
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
|
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
|
||||||
"XDTOPackage","WebService","HTTPService","WSReference",
|
"XDTOPackage","WebService","HTTPService","WSReference",
|
||||||
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
||||||
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
"FunctionalOptionsParameter","DefinedType","Bot","PaletteColor","CommonCommand","CommandGroup",
|
||||||
"Constant","CommonForm","Catalog","Document",
|
"Constant","CommonForm","Catalog","Document",
|
||||||
"DocumentNumerator","Sequence","DocumentJournal","Enum",
|
"DocumentNumerator","Sequence","DocumentJournal","Enum",
|
||||||
"Report","DataProcessor","InformationRegister","AccumulationRegister",
|
"Report","DataProcessor","InformationRegister","AccumulationRegister",
|
||||||
@@ -139,6 +140,7 @@ $childTypeDirMap = @{
|
|||||||
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"
|
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"
|
||||||
"CommonTemplate"="CommonTemplates"; "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"
|
"CommonTemplate"="CommonTemplates"; "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"
|
||||||
"Bot"="Bots"
|
"Bot"="Bots"
|
||||||
|
"PaletteColor"="PaletteColors"
|
||||||
"CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"; "XDTOPackage"="XDTOPackages"
|
"CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"; "XDTOPackage"="XDTOPackages"
|
||||||
"WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
|
"WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
|
||||||
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"
|
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"
|
||||||
+4
-4
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cf-validate v1.7 — Validate 1C configuration XML structure
|
# cf-validate v1.9 — Validate 1C configuration XML structure
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
"""Validates Configuration.xml: root structure, InternalInfo, properties, ChildObjects, languages."""
|
"""Validates Configuration.xml: root structure, InternalInfo, properties, ChildObjects, languages."""
|
||||||
import sys, os, argparse, re
|
import sys, os, argparse, re
|
||||||
@@ -59,10 +59,10 @@ VALID_CLASS_IDS = [
|
|||||||
CHILD_OBJECT_TYPES = [
|
CHILD_OBJECT_TYPES = [
|
||||||
'Language', 'Subsystem', 'StyleItem', 'Style',
|
'Language', 'Subsystem', 'StyleItem', 'Style',
|
||||||
'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate',
|
'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate',
|
||||||
'FilterCriterion', 'CommonModule', 'Bot', 'CommonAttribute', 'ExchangePlan',
|
'FilterCriterion', 'CommonModule', 'CommonAttribute', 'ExchangePlan',
|
||||||
'XDTOPackage', 'WebService', 'HTTPService', 'WSReference',
|
'XDTOPackage', 'WebService', 'HTTPService', 'WSReference',
|
||||||
'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption',
|
'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption',
|
||||||
'FunctionalOptionsParameter', 'DefinedType', 'CommonCommand', 'CommandGroup',
|
'FunctionalOptionsParameter', 'DefinedType', 'Bot', 'PaletteColor', 'CommonCommand', 'CommandGroup',
|
||||||
'Constant', 'CommonForm', 'Catalog', 'Document',
|
'Constant', 'CommonForm', 'Catalog', 'Document',
|
||||||
'DocumentNumerator', 'Sequence', 'DocumentJournal', 'Enum',
|
'DocumentNumerator', 'Sequence', 'DocumentJournal', 'Enum',
|
||||||
'Report', 'DataProcessor', 'InformationRegister', 'AccumulationRegister',
|
'Report', 'DataProcessor', 'InformationRegister', 'AccumulationRegister',
|
||||||
@@ -76,7 +76,7 @@ CHILD_TYPE_DIR_MAP = {
|
|||||||
'Language': 'Languages', 'Subsystem': 'Subsystems', 'StyleItem': 'StyleItems', 'Style': 'Styles',
|
'Language': 'Languages', 'Subsystem': 'Subsystems', 'StyleItem': 'StyleItems', 'Style': 'Styles',
|
||||||
'CommonPicture': 'CommonPictures', 'SessionParameter': 'SessionParameters', 'Role': 'Roles',
|
'CommonPicture': 'CommonPictures', 'SessionParameter': 'SessionParameters', 'Role': 'Roles',
|
||||||
'CommonTemplate': 'CommonTemplates', 'FilterCriterion': 'FilterCriteria', 'CommonModule': 'CommonModules',
|
'CommonTemplate': 'CommonTemplates', 'FilterCriterion': 'FilterCriteria', 'CommonModule': 'CommonModules',
|
||||||
'Bot': 'Bots',
|
'Bot': 'Bots', 'PaletteColor': 'PaletteColors',
|
||||||
'CommonAttribute': 'CommonAttributes', 'ExchangePlan': 'ExchangePlans', 'XDTOPackage': 'XDTOPackages',
|
'CommonAttribute': 'CommonAttributes', 'ExchangePlan': 'ExchangePlans', 'XDTOPackage': 'XDTOPackages',
|
||||||
'WebService': 'WebServices', 'HTTPService': 'HTTPServices', 'WSReference': 'WSReferences',
|
'WebService': 'WebServices', 'HTTPService': 'HTTPServices', 'WSReference': 'WSReferences',
|
||||||
'EventSubscription': 'EventSubscriptions', 'ScheduledJob': 'ScheduledJobs',
|
'EventSubscription': 'EventSubscriptions', 'ScheduledJob': 'ScheduledJobs',
|
||||||
@@ -31,6 +31,7 @@ allowed-tools:
|
|||||||
| `ExtensionPath` | Путь к каталогу расширения (обязат.) |
|
| `ExtensionPath` | Путь к каталогу расширения (обязат.) |
|
||||||
| `ConfigPath` | Путь к конфигурации-источнику (обязат.) |
|
| `ConfigPath` | Путь к конфигурации-источнику (обязат.) |
|
||||||
| `Object` | Что заимствовать (обязат.), batch через `;;` |
|
| `Object` | Что заимствовать (обязат.), batch через `;;` |
|
||||||
|
| `Module` | Создать пустые модули объекта: `ObjectModule`, `ManagerModule`, `RecordSetModule`, `ValueManagerModule` (через запятую) или `None`. У типов с единственным модулем (`CommonModule`, `HTTPService`, `WebService`) он создаётся и без параметра |
|
||||||
| `BorrowMainAttribute` | Заимствовать основной реквизит формы. Без параметра — не заимствует. `Form` — реквизиты, используемые на форме. `All` — все реквизиты объекта. Требует форму в -Object |
|
| `BorrowMainAttribute` | Заимствовать основной реквизит формы. Без параметра — не заимствует. `Form` — реквизиты, используемые на форме. `All` — все реквизиты объекта. Требует форму в -Object |
|
||||||
|
|
||||||
## Формат -Object
|
## Формат -Object
|
||||||
@@ -65,12 +66,12 @@ allowed-tools:
|
|||||||
2. `/meta-edit` — добавить новый реквизит в объект расширения
|
2. `/meta-edit` — добавить новый реквизит в объект расширения
|
||||||
3. `/form-edit` — вывести реквизит на заимствованную форму
|
3. `/form-edit` — вывести реквизит на заимствованную форму
|
||||||
|
|
||||||
**Защита существующих данных**: если зависимый объект уже заимствован с содержимым (реквизитами, формами) — скрипт не перезаписывает его, а добавляет только недостающее.
|
**Защита существующих данных**: уже заимствованный объект не перезаписывается — добавляется только недостающее. Повторный вызов безопасен: собственные реквизиты расширения, заимствованные подобъекты и код в модулях сохраняются.
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-borrow.ps1" -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Контрагенты"
|
python ".gemini/skills/cfe-borrow/scripts/cfe-borrow.py" -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Контрагенты"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
@@ -79,6 +80,12 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-borrow.ps1" -Ex
|
|||||||
# Заимствовать один объект
|
# Заимствовать один объект
|
||||||
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Контрагенты"
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Контрагенты"
|
||||||
|
|
||||||
|
# Заимствовать справочник вместе с модулями объекта и менеджера
|
||||||
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Контрагенты" -Module ObjectModule,ManagerModule
|
||||||
|
|
||||||
|
# Общий модуль без файла модуля
|
||||||
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "CommonModule.РаботаСФайлами" -Module None
|
||||||
|
|
||||||
# Заимствовать форму (автоматически заимствует родительский объект)
|
# Заимствовать форму (автоматически заимствует родительский объект)
|
||||||
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Контрагенты.Form.ФормаЭлемента"
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Контрагенты.Form.ФормаЭлемента"
|
||||||
|
|
||||||
+331
-75
@@ -1,10 +1,12 @@
|
|||||||
# cfe-borrow v1.31 — Borrow objects from configuration into extension (CFE)
|
# cfe-borrow v1.36 — Borrow objects from configuration into extension (CFE)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
[CmdletBinding(PositionalBinding=$false)]
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)][string]$ExtensionPath,
|
[Parameter(Mandatory)][string]$ExtensionPath,
|
||||||
[Parameter(Mandatory)][string]$ConfigPath,
|
[Parameter(Mandatory)][string]$ConfigPath,
|
||||||
[Parameter(Mandatory)][string]$Object,
|
[Parameter(Mandatory)][string]$Object,
|
||||||
[string]$BorrowMainAttribute
|
[string]$BorrowMainAttribute,
|
||||||
|
[string]$Module
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
@@ -258,9 +260,35 @@ $childTypeDirMap = @{
|
|||||||
"Sequence"="Sequences"; "IntegrationService"="IntegrationServices"
|
"Sequence"="Sequences"; "IntegrationService"="IntegrationServices"
|
||||||
"XDTOPackage"="XDTOPackages"; "WebService"="WebServices"
|
"XDTOPackage"="XDTOPackages"; "WebService"="WebServices"
|
||||||
"HTTPService"="HTTPServices"; "WSReference"="WSReferences"
|
"HTTPService"="HTTPServices"; "WSReference"="WSReferences"
|
||||||
"CommonAttribute"="CommonAttributes"; "Style"="Styles"; "Bot"="Bots"; "Language"="Languages"
|
"CommonAttribute"="CommonAttributes"; "Style"="Styles"; "Bot"="Bots"; "PaletteColor"="PaletteColors"; "Language"="Languages"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- 4a. Модули заимствованных объектов ---
|
||||||
|
# Порядок внутри значения — порядок выгрузки Конфигуратора: сначала «объектный» модуль
|
||||||
|
# (ObjectModule / RecordSetModule / ValueManagerModule), затем ManagerModule.
|
||||||
|
$script:moduleKindsByType = @{
|
||||||
|
"CommonModule"=@("Module"); "HTTPService"=@("Module"); "WebService"=@("Module")
|
||||||
|
"Catalog"=@("ObjectModule","ManagerModule"); "Document"=@("ObjectModule","ManagerModule")
|
||||||
|
"Report"=@("ObjectModule","ManagerModule"); "DataProcessor"=@("ObjectModule","ManagerModule")
|
||||||
|
"ExchangePlan"=@("ObjectModule","ManagerModule")
|
||||||
|
"ChartOfCharacteristicTypes"=@("ObjectModule","ManagerModule")
|
||||||
|
"ChartOfAccounts"=@("ObjectModule","ManagerModule")
|
||||||
|
"ChartOfCalculationTypes"=@("ObjectModule","ManagerModule")
|
||||||
|
"BusinessProcess"=@("ObjectModule","ManagerModule"); "Task"=@("ObjectModule","ManagerModule")
|
||||||
|
"InformationRegister"=@("RecordSetModule","ManagerModule")
|
||||||
|
"AccumulationRegister"=@("RecordSetModule","ManagerModule")
|
||||||
|
"AccountingRegister"=@("RecordSetModule","ManagerModule")
|
||||||
|
"CalculationRegister"=@("RecordSetModule","ManagerModule")
|
||||||
|
"Sequence"=@("RecordSetModule","ManagerModule")
|
||||||
|
"Constant"=@("ValueManagerModule","ManagerModule")
|
||||||
|
"Enum"=@("ManagerModule"); "DocumentJournal"=@("ManagerModule")
|
||||||
|
"FilterCriterion"=@("ManagerModule")
|
||||||
|
}
|
||||||
|
# Типы с ЕДИНСТВЕННЫМ модулем: ради него объект и заимствуют, поэтому файл создаётся молча.
|
||||||
|
# Отказ — `-Module None`.
|
||||||
|
$script:autoModuleTypes = @("CommonModule", "HTTPService", "WebService")
|
||||||
|
$script:moduleKindNames = @("Module", "ObjectModule", "ManagerModule", "RecordSetModule", "ValueManagerModule")
|
||||||
|
|
||||||
# --- 4b. Russian synonym → English type ---
|
# --- 4b. Russian synonym → English type ---
|
||||||
$synonymMap = @{
|
$synonymMap = @{
|
||||||
"Справочник"="Catalog"; "Документ"="Document"; "Перечисление"="Enum"
|
"Справочник"="Catalog"; "Документ"="Document"; "Перечисление"="Enum"
|
||||||
@@ -282,14 +310,14 @@ $synonymMap = @{
|
|||||||
"HTTPСервис"="HTTPService"; "СервисИнтеграции"="IntegrationService"
|
"HTTPСервис"="HTTPService"; "СервисИнтеграции"="IntegrationService"
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- 5. Canonical type order (44 types) ---
|
# --- 5. Canonical type order (46 types) ---
|
||||||
$script:typeOrder = @(
|
$script:typeOrder = @(
|
||||||
"Language","Subsystem","StyleItem","Style",
|
"Language","Subsystem","StyleItem","Style",
|
||||||
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
||||||
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
|
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
|
||||||
"XDTOPackage","WebService","HTTPService","WSReference",
|
"XDTOPackage","WebService","HTTPService","WSReference",
|
||||||
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
||||||
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
"FunctionalOptionsParameter","DefinedType","Bot","PaletteColor","CommonCommand","CommandGroup",
|
||||||
"Constant","CommonForm","Catalog","Document",
|
"Constant","CommonForm","Catalog","Document",
|
||||||
"DocumentNumerator","Sequence","DocumentJournal","Enum",
|
"DocumentNumerator","Sequence","DocumentJournal","Enum",
|
||||||
"Report","DataProcessor","InformationRegister","AccumulationRegister",
|
"Report","DataProcessor","InformationRegister","AccumulationRegister",
|
||||||
@@ -591,6 +619,50 @@ if ($BorrowMainAttribute) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- 9c. Validate -Module ---
|
||||||
|
$script:requestedModules = @()
|
||||||
|
$script:noModule = $false
|
||||||
|
if ($Module) {
|
||||||
|
foreach ($raw in ($Module -split '[,;]')) {
|
||||||
|
$kind = $raw.Trim()
|
||||||
|
if (-not $kind) { continue }
|
||||||
|
# Сравнение РЕГИСТРОНЕЗАВИСИМОЕ явно (-ieq): в py-порте это отдельная ветка, и молчаливое
|
||||||
|
# расхождение портов на «none» ловится только глазами.
|
||||||
|
if ($kind -ieq "None") { $script:noModule = $true; continue }
|
||||||
|
$canon = @($script:moduleKindNames | Where-Object { $_ -ieq $kind })
|
||||||
|
if ($canon.Count -eq 0) {
|
||||||
|
Write-Error "Неизвестный вид модуля '$kind'. Допустимо: $($script:moduleKindNames -join ', '), None"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
$script:requestedModules += $canon[0]
|
||||||
|
}
|
||||||
|
if ($script:noModule -and $script:requestedModules.Count -gt 0) {
|
||||||
|
Write-Error "-Module None нельзя сочетать с видами модулей"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Какие модули создать для объекта. Тип с единственным модулем получает его всегда — уточнять
|
||||||
|
# там нечего; -Module разбирает только неоднозначные типы. Иначе батч смешанных типов
|
||||||
|
# (`CommonModule.X ;; Catalog.Y`) не выражался бы одним вызовом.
|
||||||
|
function Resolve-ModuleKinds {
|
||||||
|
param([string]$typeName)
|
||||||
|
|
||||||
|
if ($script:noModule) { return @() }
|
||||||
|
$allowed = @($script:moduleKindsByType[$typeName])
|
||||||
|
if ($allowed.Count -eq 0) { return @() }
|
||||||
|
|
||||||
|
if ($script:autoModuleTypes -contains $typeName) { return @($allowed[0]) }
|
||||||
|
if ($script:requestedModules.Count -eq 0) { return @() }
|
||||||
|
|
||||||
|
# Порядок берём из таблицы типа, а не из порядка ключей в -Module.
|
||||||
|
$selected = @($allowed | Where-Object { $script:requestedModules -contains $_ })
|
||||||
|
if ($selected.Count -eq 0) {
|
||||||
|
Warn " Тип $typeName не имеет запрошенных модулей — пропущено. Допустимо: $($allowed -join ', ')"
|
||||||
|
}
|
||||||
|
return $selected
|
||||||
|
}
|
||||||
|
|
||||||
# --- 10. Helper: read source object XML ---
|
# --- 10. Helper: read source object XML ---
|
||||||
# Имена реквизитов исходного объекта → uuid. Нужны для непрозрачной формы пути в ссылках
|
# Имена реквизитов исходного объекта → uuid. Нужны для непрозрачной формы пути в ссылках
|
||||||
# параметров выбора (см. Rewrite-ChoiceParameterLinks).
|
# параметров выбора (см. Rewrite-ChoiceParameterLinks).
|
||||||
@@ -1275,32 +1347,22 @@ function Register-FormInObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Save object XML
|
# Save object XML
|
||||||
|
# Стиль исходника снимаем ДО записи: правка чужого файла наследует его BOM/EOL/заголовок
|
||||||
|
# (#44/#46/#47), новый файл получает канон выгрузки. Зеркало _detect_xml_style в py-порту.
|
||||||
|
$style2 = Detect-XmlStyle $objFile
|
||||||
$settings2 = New-Object System.Xml.XmlWriterSettings
|
$settings2 = New-Object System.Xml.XmlWriterSettings
|
||||||
$settings2.Encoding = New-Object System.Text.UTF8Encoding($true)
|
$settings2.Encoding = New-Object System.Text.UTF8Encoding($true)
|
||||||
$settings2.Indent = $false
|
$settings2.Indent = $false
|
||||||
$settings2.NewLineHandling = [System.Xml.NewLineHandling]::None
|
$settings2.NewLineHandling = [System.Xml.NewLineHandling]::None
|
||||||
|
|
||||||
$memStream2 = New-Object System.IO.MemoryStream
|
$memStream2 = New-Object System.IO.MemoryStream
|
||||||
$writer2 = [System.Xml.XmlWriter]::Create($memStream2, $settings2)
|
$writer2 = [System.Xml.XmlWriter]::Create($memStream2, $settings2)
|
||||||
$objDoc.Save($writer2)
|
$objDoc.Save($writer2)
|
||||||
$writer2.Flush(); $writer2.Close()
|
$writer2.Flush(); $writer2.Close()
|
||||||
|
$text2 = [System.Text.Encoding]::UTF8.GetString($memStream2.ToArray())
|
||||||
$bytes2 = $memStream2.ToArray()
|
|
||||||
$memStream2.Close()
|
$memStream2.Close()
|
||||||
$text2 = [System.Text.Encoding]::UTF8.GetString($bytes2)
|
$text2 = Finalize-XmlText $text2 $style2
|
||||||
if ($text2.Length -gt 0 -and $text2[0] -eq [char]0xFEFF) { $text2 = $text2.Substring(1) }
|
$writeBom2 = ($null -eq $style2) -or $style2.bom
|
||||||
$text2 = $text2.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
[System.IO.File]::WriteAllText($objFile, $text2, (New-Object System.Text.UTF8Encoding($writeBom2)))
|
||||||
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
|
||||||
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
|
||||||
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
|
||||||
$text2 = [regex]::Replace($text2, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
|
||||||
|
|
||||||
$utf8Bom2 = New-Object System.Text.UTF8Encoding($true)
|
|
||||||
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
|
||||||
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
|
||||||
$targetEol = if ((Test-Path -LiteralPath $objFile) -and ([System.IO.File]::ReadAllText($objFile) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
|
||||||
$text2 = ($text2 -replace "`r`n", "`n") -replace "`n", $targetEol
|
|
||||||
[System.IO.File]::WriteAllText($objFile, $text2, $utf8Bom2)
|
|
||||||
Info " Registered form in: $objFile"
|
Info " Registered form in: $objFile"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1313,6 +1375,81 @@ function Test-ObjectBorrowed {
|
|||||||
return (Test-Path $objFile)
|
return (Test-Path $objFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- 10f. Helper: пометка расширенного свойства (<xr:PropertyState>) ---
|
||||||
|
# Свойство появилось в формате 2.19 (8.3.26): на 2.18 и ниже платформа молча выбрасывает элемент
|
||||||
|
# при загрузке. С 2.19 Конфигуратор ставит его сам при выгрузке — эмитим, чтобы исходники навыка
|
||||||
|
# совпадали с эталоном. Имя свойства = базовое имя файла модуля (Module / ObjectModule / …),
|
||||||
|
# у заимствованной формы — Form. Ставит тот, кто создал файл модуля (или форму).
|
||||||
|
function Build-PropertyStateXml {
|
||||||
|
param([string]$propertyName, [string]$indent)
|
||||||
|
|
||||||
|
$sb = New-Object System.Text.StringBuilder
|
||||||
|
$sb.AppendLine("${indent}<xr:PropertyState>") | Out-Null
|
||||||
|
$sb.AppendLine("${indent}`t<xr:Property>${propertyName}</xr:Property>") | Out-Null
|
||||||
|
$sb.AppendLine("${indent}`t<xr:State>Extended</xr:State>") | Out-Null
|
||||||
|
$sb.Append("${indent}</xr:PropertyState>") | Out-Null
|
||||||
|
return $sb.ToString()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Set-PropertyStateFlag {
|
||||||
|
param([string]$objFile, [string]$propertyName, [string]$formatVersion)
|
||||||
|
|
||||||
|
if ((Get-FormatRank $formatVersion) -lt 219) { return }
|
||||||
|
if (-not (Test-Path $objFile)) { return }
|
||||||
|
|
||||||
|
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||||
|
$text = [System.IO.File]::ReadAllText($objFile, $enc)
|
||||||
|
$nl = if ($text -match "`r`n") { "`r`n" } else { "`n" }
|
||||||
|
|
||||||
|
# ПЕРВЫЙ <InternalInfo> в файле — собственный у объекта: у реквизитов и подобъектов свои,
|
||||||
|
# но они лежат ниже, внутри <ChildObjects>.
|
||||||
|
$empty = [regex]::Match($text, '([ \t]*)<InternalInfo\s*/>')
|
||||||
|
$open = [regex]::Match($text, '(?s)([ \t]*)<InternalInfo>(.*?)</InternalInfo>')
|
||||||
|
|
||||||
|
if ($empty.Success -and (-not $open.Success -or $empty.Index -lt $open.Index)) {
|
||||||
|
$ind = $empty.Groups[1].Value
|
||||||
|
$block = Build-PropertyStateXml $propertyName ($ind + "`t")
|
||||||
|
$replacement = "${ind}<InternalInfo>${nl}${block}${nl}${ind}</InternalInfo>"
|
||||||
|
$text = $text.Remove($empty.Index, $empty.Length).Insert($empty.Index, $replacement)
|
||||||
|
} elseif ($open.Success) {
|
||||||
|
if ($open.Groups[2].Value -match "<xr:Property>$([regex]::Escape($propertyName))</xr:Property>") { return }
|
||||||
|
$ind = $open.Groups[1].Value
|
||||||
|
$block = Build-PropertyStateXml $propertyName ($ind + "`t")
|
||||||
|
# Дописываем в КОНЕЦ InternalInfo: у Конфигуратора PropertyState идёт после GeneratedType.
|
||||||
|
$closeAt = $open.Index + $open.Length - "</InternalInfo>".Length - $ind.Length
|
||||||
|
$text = $text.Insert($closeAt, "${block}${nl}")
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
[System.IO.File]::WriteAllText($objFile, $text, $enc)
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 10g. Helper: пустой модуль заимствованного объекта ---
|
||||||
|
function New-BorrowedModuleFile {
|
||||||
|
param([string]$typeName, [string]$objName, [string]$moduleKind)
|
||||||
|
|
||||||
|
$dirName = $childTypeDirMap[$typeName]
|
||||||
|
$objDir = Join-Path (Join-Path $extDir $dirName) $objName
|
||||||
|
$moduleDir = Join-Path $objDir "Ext"
|
||||||
|
if (-not (Test-Path $moduleDir)) { New-Item -ItemType Directory -Path $moduleDir -Force | Out-Null }
|
||||||
|
|
||||||
|
# NEVER overwrite an existing one: повторное заимствование не должно затирать дописанный код
|
||||||
|
# (то же правило, что у модуля формы).
|
||||||
|
$moduleFile = Join-Path $moduleDir "${moduleKind}.bsl"
|
||||||
|
if (Test-Path $moduleFile) {
|
||||||
|
Info " Preserved existing ${moduleKind}.bsl"
|
||||||
|
} else {
|
||||||
|
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||||
|
[System.IO.File]::WriteAllText($moduleFile, "", $enc)
|
||||||
|
Info " Created: $moduleFile"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Флаг ставим и для уже существовавшего файла: состояние объекта должно отражать факт модуля.
|
||||||
|
Set-PropertyStateFlag (Join-Path (Join-Path $extDir $dirName) "${objName}.xml") $moduleKind $script:formatVersion
|
||||||
|
return $moduleFile
|
||||||
|
}
|
||||||
|
|
||||||
# --- 11. Helper: generate InternalInfo XML ---
|
# --- 11. Helper: generate InternalInfo XML ---
|
||||||
function Build-InternalInfoXml {
|
function Build-InternalInfoXml {
|
||||||
param([string]$typeName, [string]$objName, [string]$indent)
|
param([string]$typeName, [string]$objName, [string]$indent)
|
||||||
@@ -1738,6 +1875,9 @@ function Merge-AttributesIntoObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Save via text manipulation to avoid namespace issues with InnerXml
|
# Save via text manipulation to avoid namespace issues with InnerXml
|
||||||
|
# Стиль исходника снимаем ДО записи: правка чужого файла наследует его BOM/EOL/заголовок
|
||||||
|
# (#44/#46/#47), новый файл получает канон выгрузки. Зеркало _detect_xml_style в py-порту.
|
||||||
|
$style3 = Detect-XmlStyle $objFile
|
||||||
$settings3 = New-Object System.Xml.XmlWriterSettings
|
$settings3 = New-Object System.Xml.XmlWriterSettings
|
||||||
$settings3.Encoding = New-Object System.Text.UTF8Encoding($true)
|
$settings3.Encoding = New-Object System.Text.UTF8Encoding($true)
|
||||||
$settings3.Indent = $false
|
$settings3.Indent = $false
|
||||||
@@ -1746,28 +1886,15 @@ function Merge-AttributesIntoObject {
|
|||||||
$writer3 = [System.Xml.XmlWriter]::Create($memStream3, $settings3)
|
$writer3 = [System.Xml.XmlWriter]::Create($memStream3, $settings3)
|
||||||
$objDoc.Save($writer3)
|
$objDoc.Save($writer3)
|
||||||
$writer3.Flush(); $writer3.Close()
|
$writer3.Flush(); $writer3.Close()
|
||||||
$bytes3 = $memStream3.ToArray()
|
$text3 = [System.Text.Encoding]::UTF8.GetString($memStream3.ToArray())
|
||||||
$memStream3.Close()
|
$memStream3.Close()
|
||||||
$text3 = [System.Text.Encoding]::UTF8.GetString($bytes3)
|
|
||||||
if ($text3.Length -gt 0 -and $text3[0] -eq [char]0xFEFF) { $text3 = $text3.Substring(1) }
|
|
||||||
$text3 = $text3.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
|
||||||
|
|
||||||
# Самозакрытый элемент раскрывается текстом, а не пробельным узлом в DOM: тот давал
|
# Самозакрытый элемент раскрывается текстом, а не пробельным узлом в DOM: тот давал
|
||||||
# лишнюю строку с табуляцией перед первым <Attribute> (у Конфигуратора пустых строк нет).
|
# лишнюю строку с табуляцией перед первым <Attribute> (у Конфигуратора пустых строк нет).
|
||||||
|
# Стоит ДО Finalize-XmlText, чтобы схлопывание пустых тегов накрыло и вставленные реквизиты.
|
||||||
$text3 = Insert-IntoOwnChildObjects $text3 $allAttrXml
|
$text3 = Insert-IntoOwnChildObjects $text3 $allAttrXml
|
||||||
|
$text3 = Finalize-XmlText $text3 $style3
|
||||||
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
$writeBom3 = ($null -eq $style3) -or $style3.bom
|
||||||
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
[System.IO.File]::WriteAllText($objFile, $text3, (New-Object System.Text.UTF8Encoding($writeBom3)))
|
||||||
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
|
||||||
# Стоит ПОСЛЕ вставки реквизитов, чтобы накрыть и их.
|
|
||||||
$text3 = [regex]::Replace($text3, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
|
||||||
|
|
||||||
$utf8Bom3 = New-Object System.Text.UTF8Encoding($true)
|
|
||||||
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
|
||||||
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
|
||||||
$targetEol = if ((Test-Path -LiteralPath $objFile) -and ([System.IO.File]::ReadAllText($objFile) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
|
||||||
$text3 = ($text3 -replace "`r`n", "`n") -replace "`n", $targetEol
|
|
||||||
[System.IO.File]::WriteAllText($objFile, $text3, $utf8Bom3)
|
|
||||||
Info " Merged $added attribute(s) into: $objFile"
|
Info " Merged $added attribute(s) into: $objFile"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2083,6 +2210,127 @@ function Build-BorrowedObjectXml {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- 13. Helper: add object to extension ChildObjects ---
|
# --- 13. Helper: add object to extension ChildObjects ---
|
||||||
|
# Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
|
||||||
|
# финальный перенос. $null → файл новый (сохранить текущее поведение).
|
||||||
|
# Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||||
|
function Detect-XmlStyle([string]$path) {
|
||||||
|
if (-not (Test-Path -LiteralPath $path)) { return $null }
|
||||||
|
$raw = [System.IO.File]::ReadAllBytes($path)
|
||||||
|
$bom = ($raw.Length -ge 3 -and $raw[0] -eq 0xEF -and $raw[1] -eq 0xBB -and $raw[2] -eq 0xBF)
|
||||||
|
$body = if ($bom) { [System.Text.Encoding]::UTF8.GetString($raw, 3, $raw.Length - 3) } else { [System.Text.Encoding]::UTF8.GetString($raw) }
|
||||||
|
$head = if ($body.Length -gt 200) { $body.Substring(0, 200) } else { $body }
|
||||||
|
$m = [regex]::Match($head, 'encoding="([^"]+)"')
|
||||||
|
return @{
|
||||||
|
bom = $bom
|
||||||
|
crlf = $body.Contains("`r`n")
|
||||||
|
enc = $(if ($m.Success) { $m.Groups[1].Value } else { "utf-8" })
|
||||||
|
finalNl = $body.EndsWith("`n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Привести текст XmlWriter к стилю оригинала; для НОВОГО файла ($null) — к канону выгрузки
|
||||||
|
# Конфигуратора: encoding="UTF-8", CRLF, без перевода строки в конце.
|
||||||
|
# Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||||
|
function Finalize-XmlText([string]$text, $style) {
|
||||||
|
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
|
||||||
|
$encDecl = $(if ($style) { $style.enc } else { "UTF-8" })
|
||||||
|
$text = $text.Replace('encoding="utf-8"', 'encoding="' + $encDecl + '"')
|
||||||
|
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||||
|
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||||
|
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||||
|
$text = [regex]::Replace($text, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||||
|
$text = ($text -replace "`r`n", "`n").TrimEnd("`n")
|
||||||
|
if ($style -and $style.finalNl) { $text += "`n" }
|
||||||
|
if (-not $style -or $style.crlf) { $text = $text -replace "`n", "`r`n" }
|
||||||
|
return $text
|
||||||
|
}
|
||||||
|
|
||||||
|
function Find-V8Project([string]$startDir) {
|
||||||
|
$d = $startDir
|
||||||
|
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||||
|
$pj = Join-Path $d ".v8-project.json"
|
||||||
|
if (Test-Path $pj) { return $pj }
|
||||||
|
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||||
|
if ($parent -eq $d) { break }
|
||||||
|
$d = $parent
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Куда навык ставит новую запись в <ChildObjects> — настройка newObjectPosition.
|
||||||
|
# databases[].newObjectPosition базы, чей configSrc охватывает каталог родительского XML,
|
||||||
|
# иначе корневое поле, иначе end. Значения: end — после последнего объекта того же вида
|
||||||
|
# (так дописывает Конфигуратор); byName — по имени среди объектов того же вида.
|
||||||
|
# Файл ищем от рабочего каталога вверх, каталог конфигурации — запасной путь: так же
|
||||||
|
# его ищут support-guard и группа db-*, а скрипт навыка зовут по абсолютному пути, и cwd
|
||||||
|
# остаётся рабочим каталогом проекта.
|
||||||
|
# configSrc считается от каталога .v8-project.json, как задокументировано в
|
||||||
|
# docs/v8-project-guide.md. Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||||
|
function Get-NewObjectPosition([string]$cfgDir) {
|
||||||
|
try {
|
||||||
|
if (-not $cfgDir) { $cfgDir = "." }
|
||||||
|
$pj = Find-V8Project (Get-Location).Path
|
||||||
|
if (-not $pj) { $pj = Find-V8Project ([System.IO.Path]::GetFullPath($cfgDir)) }
|
||||||
|
if (-not $pj) { return "end" }
|
||||||
|
$proj = Get-Content -Raw $pj | ConvertFrom-Json
|
||||||
|
$projDir = [System.IO.Path]::GetDirectoryName($pj)
|
||||||
|
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
|
||||||
|
if ($proj.databases) {
|
||||||
|
foreach ($db in $proj.databases) {
|
||||||
|
if ($db.configSrc -and $db.newObjectPosition) {
|
||||||
|
$src = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($projDir, $db.configSrc)).TrimEnd('\', '/')
|
||||||
|
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
|
||||||
|
if ("$($db.newObjectPosition)" -eq "byName") { return "byName" }
|
||||||
|
return "end"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ("$($proj.newObjectPosition)" -eq "byName") { return "byName" }
|
||||||
|
return "end"
|
||||||
|
} catch { return "end" }
|
||||||
|
}
|
||||||
|
|
||||||
|
# Виды, у которых порядок в дереве несёт смысл: автоматически их не упорядочиваем.
|
||||||
|
# CommonAttribute — исключение самого стандарта (#std467): у общих реквизитов-разделителей
|
||||||
|
# порядок в дереве задаёт порядок установки параметров сеанса. Subsystem и CommandGroup:
|
||||||
|
# пока они не перечислены в <SubsystemsOrder> / <GroupsOrder> файла Ext/CommandInterface.xml,
|
||||||
|
# порядок дерева задаёт порядок в интерфейсе, а платформа эти списки сама не заводит
|
||||||
|
# (в выгрузке ACC вне GroupsOrder 15 живых групп из 39). Language исключён из осторожности,
|
||||||
|
# без замера: языков обычно один-два, и в типовых их порядок не алфавитный.
|
||||||
|
# Явно названный вид сортируется в любом случае.
|
||||||
|
# Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||||
|
function Test-OrderSensitiveType([string]$typeName) {
|
||||||
|
return @("CommonAttribute", "Subsystem", "CommandGroup", "Language") -ccontains $typeName
|
||||||
|
}
|
||||||
|
|
||||||
|
# Порядок имён объектов метаданных, как в дереве Конфигуратора.
|
||||||
|
# Ключ — пары «ранг+символ»: регистр не учитывается, подчёркивание раньше цифр, цифры раньше
|
||||||
|
# букв, буквы по кодам (латиница раньше кириллицы), ё на месте е. Культурные таблицы не
|
||||||
|
# используются — они разные на разных ОС и в разных рантаймах, а так оба порта сравнивают
|
||||||
|
# одинаково везде. Равные ключи разводит ordinal-сравнение исходных строк.
|
||||||
|
# Возвращает -1 | 0 | 1. Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||||
|
function Compare-MetadataNames([string]$a, [string]$b) {
|
||||||
|
$keys = @("", "")
|
||||||
|
$names = @($a, $b)
|
||||||
|
for ($i = 0; $i -lt 2; $i++) {
|
||||||
|
$sb = New-Object System.Text.StringBuilder
|
||||||
|
foreach ($ch in $names[$i].ToLowerInvariant().ToCharArray()) {
|
||||||
|
if ($ch -eq [char]0x0451) { $ch = [char]0x0435 }
|
||||||
|
if ([char]::IsDigit($ch)) { [void]$sb.Append('1') }
|
||||||
|
elseif ([char]::IsLetter($ch)) { [void]$sb.Append('2') }
|
||||||
|
else { [void]$sb.Append('0') }
|
||||||
|
[void]$sb.Append($ch)
|
||||||
|
}
|
||||||
|
$keys[$i] = $sb.ToString()
|
||||||
|
}
|
||||||
|
$r = [string]::CompareOrdinal($keys[0], $keys[1])
|
||||||
|
if ($r -eq 0) { $r = [string]::CompareOrdinal($a, $b) }
|
||||||
|
if ($r -lt 0) { return -1 }
|
||||||
|
if ($r -gt 0) { return 1 }
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
function Add-ToChildObjects {
|
function Add-ToChildObjects {
|
||||||
param([string]$typeName, [string]$objName)
|
param([string]$typeName, [string]$objName)
|
||||||
|
|
||||||
@@ -2108,7 +2356,12 @@ function Add-ToChildObjects {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Find insertion point: after last element of same type, or before first element of later type
|
# Место вставки. Вид — по $script:typeOrder; внутри вида — по newObjectPosition: end
|
||||||
|
# (по умолчанию) кладёт после последнего объекта того же вида, byName — по имени. Так же
|
||||||
|
# заимствует Конфигуратор: в боевых выгрузках расширений ChildObjects не отсортирован.
|
||||||
|
# Subsystem по имени не упорядочиваем никогда: порядок подсистем в дереве задаёт порядок
|
||||||
|
# разделов в панели.
|
||||||
|
$byName = (-not (Test-OrderSensitiveType $typeName) -and (Get-NewObjectPosition $extDir) -eq "byName")
|
||||||
$insertBefore = $null
|
$insertBefore = $null
|
||||||
$lastSameType = $null
|
$lastSameType = $null
|
||||||
|
|
||||||
@@ -2118,8 +2371,7 @@ function Add-ToChildObjects {
|
|||||||
if ($childTypeIdx -lt 0) { continue }
|
if ($childTypeIdx -lt 0) { continue }
|
||||||
|
|
||||||
if ($child.LocalName -eq $typeName) {
|
if ($child.LocalName -eq $typeName) {
|
||||||
# Same type -- check alphabetical order
|
if ($byName -and -not $insertBefore -and (Compare-MetadataNames $child.InnerText $objName) -gt 0) {
|
||||||
if ($child.InnerText -gt $objName -and -not $insertBefore) {
|
|
||||||
$insertBefore = $child
|
$insertBefore = $child
|
||||||
}
|
}
|
||||||
$lastSameType = $child
|
$lastSameType = $child
|
||||||
@@ -2204,6 +2456,9 @@ foreach ($item in $items) {
|
|||||||
$hasBMA = [bool]$BorrowMainAttribute
|
$hasBMA = [bool]$BorrowMainAttribute
|
||||||
$formFiles = Borrow-Form $typeName $objName $formName -BorrowMainAttr:$hasBMA
|
$formFiles = Borrow-Form $typeName $objName $formName -BorrowMainAttr:$hasBMA
|
||||||
$script:borrowedFiles += $formFiles
|
$script:borrowedFiles += $formFiles
|
||||||
|
# Замер на 8.3.26: платформа помечает форму расширенной сразу при заимствовании,
|
||||||
|
# даже если элементы не менялись. Флаг живёт в метаданных формы, не у владельца.
|
||||||
|
Set-PropertyStateFlag $formFiles[0] "Form" $script:formatVersion
|
||||||
$borrowedCount++
|
$borrowedCount++
|
||||||
|
|
||||||
# Borrow main attribute if requested
|
# Borrow main attribute if requested
|
||||||
@@ -2212,26 +2467,37 @@ foreach ($item in $items) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
# --- Object borrowing (existing logic) ---
|
# --- Object borrowing (existing logic) ---
|
||||||
Info "Borrowing ${typeName}.${objName}..."
|
|
||||||
|
|
||||||
$src = Read-SourceObject $typeName $objName
|
|
||||||
Info " Source UUID: $($src.Uuid)"
|
|
||||||
|
|
||||||
$borrowedXml = Build-BorrowedObjectXml $typeName $objName $src.Uuid $src.Properties
|
|
||||||
|
|
||||||
$targetDir = Join-Path $extDir $dirName
|
$targetDir = Join-Path $extDir $dirName
|
||||||
if (-not (Test-Path $targetDir)) {
|
|
||||||
New-Item -ItemType Directory -Path $targetDir -Force | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
$targetFile = Join-Path $targetDir "${objName}.xml"
|
$targetFile = Join-Path $targetDir "${objName}.xml"
|
||||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
|
||||||
[System.IO.File]::WriteAllText($targetFile, $borrowedXml, $enc)
|
# Уже заимствованный объект НЕ переписываем: в его XML лежат собственные реквизиты
|
||||||
Info " Created: $targetFile"
|
# расширения, заимствованные подобъекты и состояния, которые из источника не выводятся.
|
||||||
|
# Повторный вызов — законный способ доделать модуль (-Module), а не переиздать заготовку.
|
||||||
|
if (Test-ObjectBorrowed $typeName $objName) {
|
||||||
|
Info "Already borrowed: ${typeName}.${objName} — XML сохранён без изменений"
|
||||||
|
} else {
|
||||||
|
Info "Borrowing ${typeName}.${objName}..."
|
||||||
|
|
||||||
|
$src = Read-SourceObject $typeName $objName
|
||||||
|
Info " Source UUID: $($src.Uuid)"
|
||||||
|
|
||||||
|
$borrowedXml = Build-BorrowedObjectXml $typeName $objName $src.Uuid $src.Properties
|
||||||
|
|
||||||
|
if (-not (Test-Path $targetDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $targetDir -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||||
|
[System.IO.File]::WriteAllText($targetFile, $borrowedXml, $enc)
|
||||||
|
Info " Created: $targetFile"
|
||||||
|
}
|
||||||
|
|
||||||
Add-ToChildObjects $typeName $objName
|
Add-ToChildObjects $typeName $objName
|
||||||
|
|
||||||
$script:borrowedFiles += $targetFile
|
$script:borrowedFiles += $targetFile
|
||||||
|
foreach ($kind in (Resolve-ModuleKinds $typeName)) {
|
||||||
|
$script:borrowedFiles += (New-BorrowedModuleFile $typeName $objName $kind)
|
||||||
|
}
|
||||||
$borrowedCount++
|
$borrowedCount++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2278,32 +2544,22 @@ while ($true) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- 15. Save modified Configuration.xml ---
|
# --- 15. Save modified Configuration.xml ---
|
||||||
|
# Стиль исходника снимаем ДО записи: правка чужого файла наследует его BOM/EOL/заголовок
|
||||||
|
# (#44/#46/#47), новый файл получает канон выгрузки. Зеркало _detect_xml_style в py-порту.
|
||||||
|
$style = Detect-XmlStyle $extResolvedPath
|
||||||
$settings = New-Object System.Xml.XmlWriterSettings
|
$settings = New-Object System.Xml.XmlWriterSettings
|
||||||
$settings.Encoding = New-Object System.Text.UTF8Encoding($true)
|
$settings.Encoding = New-Object System.Text.UTF8Encoding($true)
|
||||||
$settings.Indent = $false
|
$settings.Indent = $false
|
||||||
$settings.NewLineHandling = [System.Xml.NewLineHandling]::None
|
$settings.NewLineHandling = [System.Xml.NewLineHandling]::None
|
||||||
|
|
||||||
$memStream = New-Object System.IO.MemoryStream
|
$memStream = New-Object System.IO.MemoryStream
|
||||||
$writer = [System.Xml.XmlWriter]::Create($memStream, $settings)
|
$writer = [System.Xml.XmlWriter]::Create($memStream, $settings)
|
||||||
$script:xmlDoc.Save($writer)
|
$script:xmlDoc.Save($writer)
|
||||||
$writer.Flush(); $writer.Close()
|
$writer.Flush(); $writer.Close()
|
||||||
|
$text = [System.Text.Encoding]::UTF8.GetString($memStream.ToArray())
|
||||||
$bytes = $memStream.ToArray()
|
|
||||||
$memStream.Close()
|
$memStream.Close()
|
||||||
$text = [System.Text.Encoding]::UTF8.GetString($bytes)
|
$text = Finalize-XmlText $text $style
|
||||||
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
|
$writeBom = ($null -eq $style) -or $style.bom
|
||||||
$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
[System.IO.File]::WriteAllText($extResolvedPath, $text, (New-Object System.Text.UTF8Encoding($writeBom)))
|
||||||
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
|
||||||
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
|
||||||
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
|
||||||
$text = [regex]::Replace($text, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
|
||||||
|
|
||||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
|
||||||
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
|
||||||
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
|
||||||
$targetEol = if ((Test-Path -LiteralPath $extResolvedPath) -and ([System.IO.File]::ReadAllText($extResolvedPath) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
|
||||||
$text = ($text -replace "`r`n", "`n") -replace "`n", $targetEol
|
|
||||||
[System.IO.File]::WriteAllText($extResolvedPath, $text, $utf8Bom)
|
|
||||||
Info "Saved: $extResolvedPath"
|
Info "Saved: $extResolvedPath"
|
||||||
|
|
||||||
# --- 16. Summary ---
|
# --- 16. Summary ---
|
||||||
+264
-16
@@ -1,8 +1,9 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cfe-borrow v1.31 — Borrow objects from configuration into extension (CFE)
|
# cfe-borrow v1.36 — Borrow objects from configuration into extension (CFE)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
@@ -202,6 +203,97 @@ def decode_numeric_entities(s):
|
|||||||
return s
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def _sg_find_v8project(start_dir):
|
||||||
|
d = start_dir
|
||||||
|
for _ in range(20):
|
||||||
|
if not d:
|
||||||
|
break
|
||||||
|
pj = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pj):
|
||||||
|
return pj
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
break
|
||||||
|
d = parent
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_new_object_position(cfg_dir):
|
||||||
|
"""Куда навык ставит новую запись в <ChildObjects> — настройка newObjectPosition.
|
||||||
|
|
||||||
|
databases[].newObjectPosition базы, чей configSrc охватывает каталог родительского XML,
|
||||||
|
иначе корневое поле, иначе end. Значения: end — после последнего объекта того же вида
|
||||||
|
(так дописывает Конфигуратор); byName — по имени среди объектов того же вида.
|
||||||
|
Файл ищем от рабочего каталога вверх, каталог конфигурации — запасной путь: так же
|
||||||
|
его ищут support-guard и группа db-*, а скрипт навыка зовут по абсолютному пути, и cwd
|
||||||
|
остаётся рабочим каталогом проекта.
|
||||||
|
configSrc считается от каталога .v8-project.json, как задокументировано в
|
||||||
|
docs/v8-project-guide.md. Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(os.path.abspath(cfg_dir or "."))
|
||||||
|
if not pj:
|
||||||
|
return "end"
|
||||||
|
proj = json.loads(open(pj, encoding="utf-8-sig").read())
|
||||||
|
proj_dir = os.path.dirname(pj)
|
||||||
|
cfg_full = os.path.normcase(os.path.abspath(cfg_dir or ".")).rstrip("\\/")
|
||||||
|
for db in proj.get("databases", []):
|
||||||
|
src = db.get("configSrc")
|
||||||
|
if src and db.get("newObjectPosition"):
|
||||||
|
src_full = os.path.normcase(os.path.abspath(os.path.join(proj_dir, src))).rstrip("\\/")
|
||||||
|
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
|
||||||
|
return "byName" if str(db["newObjectPosition"]).lower() == "byname" else "end"
|
||||||
|
if str(proj.get("newObjectPosition") or "").lower() == "byname":
|
||||||
|
return "byName"
|
||||||
|
return "end"
|
||||||
|
except Exception:
|
||||||
|
return "end"
|
||||||
|
|
||||||
|
|
||||||
|
def is_order_sensitive_type(type_name):
|
||||||
|
"""Виды, у которых порядок в дереве несёт смысл: автоматически их не упорядочиваем.
|
||||||
|
|
||||||
|
CommonAttribute — исключение самого стандарта (#std467): у общих реквизитов-разделителей
|
||||||
|
порядок в дереве задаёт порядок установки параметров сеанса. Subsystem и CommandGroup:
|
||||||
|
пока они не перечислены в <SubsystemsOrder> / <GroupsOrder> файла Ext/CommandInterface.xml,
|
||||||
|
порядок дерева задаёт порядок в интерфейсе, а платформа эти списки сама не заводит
|
||||||
|
(в выгрузке ACC вне GroupsOrder 15 живых групп из 39). Language исключён из осторожности,
|
||||||
|
без замера: языков обычно один-два, и в типовых их порядок не алфавитный.
|
||||||
|
Явно названный вид сортируется в любом случае.
|
||||||
|
Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||||
|
"""
|
||||||
|
return type_name in ("CommonAttribute", "Subsystem", "CommandGroup", "Language")
|
||||||
|
|
||||||
|
|
||||||
|
def compare_metadata_names(a, b):
|
||||||
|
"""Порядок имён объектов метаданных, как в дереве Конфигуратора.
|
||||||
|
|
||||||
|
Ключ — пары «ранг+символ»: регистр не учитывается, подчёркивание раньше цифр, цифры раньше
|
||||||
|
букв, буквы по кодам (латиница раньше кириллицы), ё на месте е. Культурные таблицы не
|
||||||
|
используются — они разные на разных ОС и в разных рантаймах, а так оба порта сравнивают
|
||||||
|
одинаково везде. Равные ключи разводит ordinal-сравнение исходных строк.
|
||||||
|
Возвращает -1 | 0 | 1. Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||||
|
"""
|
||||||
|
keys = []
|
||||||
|
for name in (a, b):
|
||||||
|
parts = []
|
||||||
|
for ch in name.lower():
|
||||||
|
if ch == "ё":
|
||||||
|
ch = "е"
|
||||||
|
if ch.isdigit():
|
||||||
|
parts.append("1" + ch)
|
||||||
|
elif ch.isalpha():
|
||||||
|
parts.append("2" + ch)
|
||||||
|
else:
|
||||||
|
parts.append("0" + ch)
|
||||||
|
keys.append("".join(parts))
|
||||||
|
if keys[0] != keys[1]:
|
||||||
|
return -1 if keys[0] < keys[1] else 1
|
||||||
|
if a != b:
|
||||||
|
return -1 if a < b else 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def localname(el):
|
def localname(el):
|
||||||
return etree.QName(el.tag).localname
|
return etree.QName(el.tag).localname
|
||||||
|
|
||||||
@@ -237,9 +329,35 @@ CHILD_TYPE_DIR_MAP = {
|
|||||||
"XDTOPackage": "XDTOPackages", "WebService": "WebServices",
|
"XDTOPackage": "XDTOPackages", "WebService": "WebServices",
|
||||||
"HTTPService": "HTTPServices", "WSReference": "WSReferences",
|
"HTTPService": "HTTPServices", "WSReference": "WSReferences",
|
||||||
"CommonAttribute": "CommonAttributes", "Style": "Styles",
|
"CommonAttribute": "CommonAttributes", "Style": "Styles",
|
||||||
"Bot": "Bots", "Language": "Languages",
|
"Bot": "Bots", "PaletteColor": "PaletteColors", "PaletteColor": "PaletteColors", "Language": "Languages",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Модули заимствованных объектов ---
|
||||||
|
# Порядок внутри значения — порядок выгрузки Конфигуратора: сначала «объектный» модуль
|
||||||
|
# (ObjectModule / RecordSetModule / ValueManagerModule), затем ManagerModule.
|
||||||
|
MODULE_KINDS_BY_TYPE = {
|
||||||
|
"CommonModule": ["Module"], "HTTPService": ["Module"], "WebService": ["Module"],
|
||||||
|
"Catalog": ["ObjectModule", "ManagerModule"], "Document": ["ObjectModule", "ManagerModule"],
|
||||||
|
"Report": ["ObjectModule", "ManagerModule"], "DataProcessor": ["ObjectModule", "ManagerModule"],
|
||||||
|
"ExchangePlan": ["ObjectModule", "ManagerModule"],
|
||||||
|
"ChartOfCharacteristicTypes": ["ObjectModule", "ManagerModule"],
|
||||||
|
"ChartOfAccounts": ["ObjectModule", "ManagerModule"],
|
||||||
|
"ChartOfCalculationTypes": ["ObjectModule", "ManagerModule"],
|
||||||
|
"BusinessProcess": ["ObjectModule", "ManagerModule"], "Task": ["ObjectModule", "ManagerModule"],
|
||||||
|
"InformationRegister": ["RecordSetModule", "ManagerModule"],
|
||||||
|
"AccumulationRegister": ["RecordSetModule", "ManagerModule"],
|
||||||
|
"AccountingRegister": ["RecordSetModule", "ManagerModule"],
|
||||||
|
"CalculationRegister": ["RecordSetModule", "ManagerModule"],
|
||||||
|
"Sequence": ["RecordSetModule", "ManagerModule"],
|
||||||
|
"Constant": ["ValueManagerModule", "ManagerModule"],
|
||||||
|
"Enum": ["ManagerModule"], "DocumentJournal": ["ManagerModule"],
|
||||||
|
"FilterCriterion": ["ManagerModule"],
|
||||||
|
}
|
||||||
|
# Типы с ЕДИНСТВЕННЫМ модулем: ради него объект и заимствуют, поэтому файл создаётся молча.
|
||||||
|
# Отказ — `-Module None`.
|
||||||
|
AUTO_MODULE_TYPES = ["CommonModule", "HTTPService", "WebService"]
|
||||||
|
MODULE_KIND_NAMES = ["Module", "ObjectModule", "ManagerModule", "RecordSetModule", "ValueManagerModule"]
|
||||||
|
|
||||||
SYNONYM_MAP = {
|
SYNONYM_MAP = {
|
||||||
"\u0421\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a": "Catalog",
|
"\u0421\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a": "Catalog",
|
||||||
"\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442": "Document",
|
"\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442": "Document",
|
||||||
@@ -281,10 +399,10 @@ SYNONYM_MAP = {
|
|||||||
TYPE_ORDER = [
|
TYPE_ORDER = [
|
||||||
"Language", "Subsystem", "StyleItem", "Style",
|
"Language", "Subsystem", "StyleItem", "Style",
|
||||||
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
|
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
|
||||||
"FilterCriterion", "CommonModule", "Bot", "CommonAttribute", "ExchangePlan",
|
"FilterCriterion", "CommonModule", "CommonAttribute", "ExchangePlan",
|
||||||
"XDTOPackage", "WebService", "HTTPService", "WSReference",
|
"XDTOPackage", "WebService", "HTTPService", "WSReference",
|
||||||
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
|
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
|
||||||
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
|
"FunctionalOptionsParameter", "DefinedType", "Bot", "PaletteColor", "CommonCommand", "CommandGroup",
|
||||||
"Constant", "CommonForm", "Catalog", "Document",
|
"Constant", "CommonForm", "Catalog", "Document",
|
||||||
"DocumentNumerator", "Sequence", "DocumentJournal", "Enum",
|
"DocumentNumerator", "Sequence", "DocumentJournal", "Enum",
|
||||||
"Report", "DataProcessor", "InformationRegister", "AccumulationRegister",
|
"Report", "DataProcessor", "InformationRegister", "AccumulationRegister",
|
||||||
@@ -516,6 +634,55 @@ def format_rank(ver):
|
|||||||
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
|
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
|
||||||
|
|
||||||
|
|
||||||
|
# --- Пометка расширенного свойства (<xr:PropertyState>) ---
|
||||||
|
# Свойство появилось в формате 2.19 (8.3.26): на 2.18 и ниже платформа молча выбрасывает элемент
|
||||||
|
# при загрузке. С 2.19 Конфигуратор ставит его сам при выгрузке. Правило: флаг ставит тот, кто
|
||||||
|
# создал файл модуля, — здесь это мы. Имя свойства = базовое имя файла модуля.
|
||||||
|
# Копии этих функций есть в cfe-patch-method (навыки автономны); держать их одинаковыми — сознательно.
|
||||||
|
def build_property_state_xml(property_name, indent):
|
||||||
|
return "\n".join([
|
||||||
|
f"{indent}<xr:PropertyState>",
|
||||||
|
f"{indent}\t<xr:Property>{property_name}</xr:Property>",
|
||||||
|
f"{indent}\t<xr:State>Extended</xr:State>",
|
||||||
|
f"{indent}</xr:PropertyState>",
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
def set_property_state_flag(obj_file, property_name, format_version):
|
||||||
|
if format_rank(format_version) < 219:
|
||||||
|
return
|
||||||
|
if not os.path.isfile(obj_file):
|
||||||
|
return
|
||||||
|
|
||||||
|
with open(obj_file, "r", encoding="utf-8-sig", newline="") as fh:
|
||||||
|
text = fh.read()
|
||||||
|
nl = "\r\n" if "\r\n" in text else "\n"
|
||||||
|
|
||||||
|
# ПЕРВЫЙ <InternalInfo> в файле — собственный у объекта: у реквизитов и подобъектов свои,
|
||||||
|
# но они лежат ниже, внутри <ChildObjects>.
|
||||||
|
empty = re.search(r"([ \t]*)<InternalInfo\s*/>", text)
|
||||||
|
opened = re.search(r"([ \t]*)<InternalInfo>(.*?)</InternalInfo>", text, re.S)
|
||||||
|
|
||||||
|
if empty and (not opened or empty.start() < opened.start()):
|
||||||
|
ind = empty.group(1)
|
||||||
|
block = build_property_state_xml(property_name, ind + "\t")
|
||||||
|
replacement = f"{ind}<InternalInfo>{nl}{block}{nl}{ind}</InternalInfo>"
|
||||||
|
text = text[:empty.start()] + replacement + text[empty.end():]
|
||||||
|
elif opened:
|
||||||
|
if re.search(rf"<xr:Property>{re.escape(property_name)}</xr:Property>", opened.group(2)):
|
||||||
|
return
|
||||||
|
ind = opened.group(1)
|
||||||
|
block = build_property_state_xml(property_name, ind + "\t")
|
||||||
|
# Дописываем в КОНЕЦ InternalInfo: у Конфигуратора PropertyState идёт после GeneratedType.
|
||||||
|
close_at = opened.end() - len("</InternalInfo>") - len(ind)
|
||||||
|
text = text[:close_at] + block + nl + text[close_at:]
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
with open(obj_file, "w", encoding="utf-8-sig", newline="") as fh:
|
||||||
|
fh.write(text)
|
||||||
|
|
||||||
|
|
||||||
def apply_pal_ns(format_version):
|
def apply_pal_ns(format_version):
|
||||||
"""2.21 (8.5) добавила в шапку пространство палитры — ради <Color> у значений перечисления.
|
"""2.21 (8.5) добавила в шапку пространство палитры — ради <Color> у значений перечисления.
|
||||||
Вставляем НА МЕСТО (после lf, перед style): платформа держит объявления по алфавиту,
|
Вставляем НА МЕСТО (после lf, перед style): платформа держит объявления по алфавиту,
|
||||||
@@ -655,6 +822,7 @@ def main():
|
|||||||
parser.add_argument("-ConfigPath", required=True)
|
parser.add_argument("-ConfigPath", required=True)
|
||||||
parser.add_argument("-Object", required=True)
|
parser.add_argument("-Object", required=True)
|
||||||
parser.add_argument("-BorrowMainAttribute", nargs="?", const="Form", default=None)
|
parser.add_argument("-BorrowMainAttribute", nargs="?", const="Form", default=None)
|
||||||
|
parser.add_argument("-Module", default=None)
|
||||||
args = ci_parse_args(parser)
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
# --- 1. Resolve paths ---
|
# --- 1. Resolve paths ---
|
||||||
@@ -860,6 +1028,25 @@ def main():
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return src_uuid
|
return src_uuid
|
||||||
|
|
||||||
|
# --- Пустой модуль заимствованного объекта ---
|
||||||
|
def new_borrowed_module_file(type_name, obj_name, module_kind):
|
||||||
|
dir_name = CHILD_TYPE_DIR_MAP[type_name]
|
||||||
|
module_dir = os.path.join(ext_dir, dir_name, obj_name, "Ext")
|
||||||
|
os.makedirs(module_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# NEVER overwrite an existing one: повторное заимствование не должно затирать дописанный
|
||||||
|
# код (то же правило, что у модуля формы).
|
||||||
|
module_file = os.path.join(module_dir, f"{module_kind}.bsl")
|
||||||
|
if os.path.isfile(module_file):
|
||||||
|
info(f" Preserved existing {module_kind}.bsl")
|
||||||
|
else:
|
||||||
|
write_utf8_bom(module_file, "")
|
||||||
|
info(f" Created: {module_file}")
|
||||||
|
|
||||||
|
# Флаг ставим и для уже существовавшего файла: состояние объекта должно отражать факт модуля.
|
||||||
|
set_property_state_flag(os.path.join(ext_dir, dir_name, f"{obj_name}.xml"), module_kind, format_version)
|
||||||
|
return module_file
|
||||||
|
|
||||||
def build_internal_info_xml(type_name, obj_name, indent):
|
def build_internal_info_xml(type_name, obj_name, indent):
|
||||||
types = GENERATED_TYPES.get(type_name)
|
types = GENERATED_TYPES.get(type_name)
|
||||||
if not types:
|
if not types:
|
||||||
@@ -940,6 +1127,13 @@ def main():
|
|||||||
warn(f"Already in ChildObjects: {type_name}.{obj_name}")
|
warn(f"Already in ChildObjects: {type_name}.{obj_name}")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Место вставки. Вид — по TYPE_ORDER; внутри вида — по newObjectPosition:
|
||||||
|
# end (по умолчанию) кладёт после последнего объекта того же вида, byName — по имени.
|
||||||
|
# Так же, как заимствует Конфигуратор: в боевых выгрузках расширений ChildObjects
|
||||||
|
# не отсортирован. Subsystem по имени не упорядочиваем никогда: порядок подсистем
|
||||||
|
# в дереве задаёт порядок разделов в панели.
|
||||||
|
by_name = (not is_order_sensitive_type(type_name)
|
||||||
|
and get_new_object_position(ext_dir) == "byName")
|
||||||
insert_before = None
|
insert_before = None
|
||||||
for child in child_objs_el:
|
for child in child_objs_el:
|
||||||
if not isinstance(child.tag, str):
|
if not isinstance(child.tag, str):
|
||||||
@@ -950,7 +1144,8 @@ def main():
|
|||||||
child_type_idx = TYPE_ORDER.index(child_type_name)
|
child_type_idx = TYPE_ORDER.index(child_type_name)
|
||||||
|
|
||||||
if child_type_name == type_name:
|
if child_type_name == type_name:
|
||||||
if (child.text or "") > obj_name and insert_before is None:
|
if (by_name and insert_before is None
|
||||||
|
and compare_metadata_names(child.text or "", obj_name) > 0):
|
||||||
insert_before = child
|
insert_before = child
|
||||||
elif child_type_idx > type_idx and insert_before is None:
|
elif child_type_idx > type_idx and insert_before is None:
|
||||||
insert_before = child
|
insert_before = child
|
||||||
@@ -2019,6 +2214,47 @@ def main():
|
|||||||
print("-BorrowMainAttribute requires a form in -Object (e.g. 'Catalog.X.Form.Y')", file=sys.stderr)
|
print("-BorrowMainAttribute requires a form in -Object (e.g. 'Catalog.X.Form.Y')", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
# --- 9c. Validate -Module ---
|
||||||
|
requested_modules = []
|
||||||
|
no_module = False
|
||||||
|
if args.Module:
|
||||||
|
for raw in re.split(r"[,;]", args.Module):
|
||||||
|
kind = raw.strip()
|
||||||
|
if not kind:
|
||||||
|
continue
|
||||||
|
# Сравнение РЕГИСТРОНЕЗАВИСИМОЕ явно: в ps1-порте `-ieq`, и молчаливое расхождение
|
||||||
|
# портов на «none» ловится только глазами.
|
||||||
|
if kind.lower() == "none":
|
||||||
|
no_module = True
|
||||||
|
continue
|
||||||
|
canon = [k for k in MODULE_KIND_NAMES if k.lower() == kind.lower()]
|
||||||
|
if not canon:
|
||||||
|
print(f"Неизвестный вид модуля '{kind}'. Допустимо: {', '.join(MODULE_KIND_NAMES)}, None", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
requested_modules.append(canon[0])
|
||||||
|
if no_module and requested_modules:
|
||||||
|
print("-Module None нельзя сочетать с видами модулей", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Какие модули создать для объекта. Тип с единственным модулем получает его всегда — уточнять
|
||||||
|
# там нечего; -Module разбирает только неоднозначные типы. Иначе батч смешанных типов
|
||||||
|
# (`CommonModule.X ;; Catalog.Y`) не выражался бы одним вызовом.
|
||||||
|
def resolve_module_kinds(type_name):
|
||||||
|
if no_module:
|
||||||
|
return []
|
||||||
|
allowed = MODULE_KINDS_BY_TYPE.get(type_name, [])
|
||||||
|
if not allowed:
|
||||||
|
return []
|
||||||
|
if type_name in AUTO_MODULE_TYPES:
|
||||||
|
return [allowed[0]]
|
||||||
|
if not requested_modules:
|
||||||
|
return []
|
||||||
|
# Порядок берём из таблицы типа, а не из порядка ключей в -Module.
|
||||||
|
selected = [k for k in allowed if k in requested_modules]
|
||||||
|
if not selected:
|
||||||
|
warn(f" Тип {type_name} не имеет запрошенных модулей — пропущено. Допустимо: {', '.join(allowed)}")
|
||||||
|
return selected
|
||||||
|
|
||||||
# --- 10. Process each item ---
|
# --- 10. Process each item ---
|
||||||
borrowed_count = 0
|
borrowed_count = 0
|
||||||
|
|
||||||
@@ -2070,6 +2306,9 @@ def main():
|
|||||||
has_bma = borrow_main_attribute_mode is not None
|
has_bma = borrow_main_attribute_mode is not None
|
||||||
form_files = borrow_form(type_name, obj_name, form_name, borrow_main_attr=has_bma)
|
form_files = borrow_form(type_name, obj_name, form_name, borrow_main_attr=has_bma)
|
||||||
borrowed_files.extend(form_files)
|
borrowed_files.extend(form_files)
|
||||||
|
# Замер на 8.3.26: платформа помечает форму расширенной сразу при заимствовании,
|
||||||
|
# даже если элементы не менялись. Флаг живёт в метаданных формы, не у владельца.
|
||||||
|
set_property_state_flag(form_files[0], "Form", format_version)
|
||||||
borrowed_count += 1
|
borrowed_count += 1
|
||||||
|
|
||||||
# Borrow main attribute if requested
|
# Borrow main attribute if requested
|
||||||
@@ -2077,23 +2316,32 @@ def main():
|
|||||||
borrow_main_attribute(type_name, obj_name, form_name, borrow_main_attribute_mode)
|
borrow_main_attribute(type_name, obj_name, form_name, borrow_main_attribute_mode)
|
||||||
else:
|
else:
|
||||||
# --- Object borrowing ---
|
# --- Object borrowing ---
|
||||||
info(f"Borrowing {type_name}.{obj_name}...")
|
|
||||||
|
|
||||||
src = read_source_object(type_name, obj_name)
|
|
||||||
info(f" Source UUID: {src['Uuid']}")
|
|
||||||
|
|
||||||
borrowed_xml = build_borrowed_object_xml(type_name, obj_name, src["Uuid"], src["Properties"])
|
|
||||||
|
|
||||||
target_dir = os.path.join(ext_dir, dir_name)
|
target_dir = os.path.join(ext_dir, dir_name)
|
||||||
os.makedirs(target_dir, exist_ok=True)
|
|
||||||
|
|
||||||
target_file = os.path.join(target_dir, f"{obj_name}.xml")
|
target_file = os.path.join(target_dir, f"{obj_name}.xml")
|
||||||
write_xml_file(target_file, borrowed_xml)
|
|
||||||
info(f" Created: {target_file}")
|
# Уже заимствованный объект НЕ переписываем: в его XML лежат собственные реквизиты
|
||||||
|
# расширения, заимствованные подобъекты и состояния, которые из источника не
|
||||||
|
# выводятся. Повторный вызов — законный способ доделать модуль (-Module), а не
|
||||||
|
# переиздать заготовку.
|
||||||
|
if test_object_borrowed(type_name, obj_name):
|
||||||
|
info(f"Already borrowed: {type_name}.{obj_name} — XML сохранён без изменений")
|
||||||
|
else:
|
||||||
|
info(f"Borrowing {type_name}.{obj_name}...")
|
||||||
|
|
||||||
|
src = read_source_object(type_name, obj_name)
|
||||||
|
info(f" Source UUID: {src['Uuid']}")
|
||||||
|
|
||||||
|
borrowed_xml = build_borrowed_object_xml(type_name, obj_name, src["Uuid"], src["Properties"])
|
||||||
|
|
||||||
|
os.makedirs(target_dir, exist_ok=True)
|
||||||
|
write_xml_file(target_file, borrowed_xml)
|
||||||
|
info(f" Created: {target_file}")
|
||||||
|
|
||||||
add_to_child_objects(type_name, obj_name)
|
add_to_child_objects(type_name, obj_name)
|
||||||
|
|
||||||
borrowed_files.append(target_file)
|
borrowed_files.append(target_file)
|
||||||
|
for kind in resolve_module_kinds(type_name):
|
||||||
|
borrowed_files.append(new_borrowed_module_file(type_name, obj_name, kind))
|
||||||
borrowed_count += 1
|
borrowed_count += 1
|
||||||
|
|
||||||
# --- Владельцы заимствованных справочников ---
|
# --- Владельцы заимствованных справочников ---
|
||||||
@@ -23,7 +23,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-diff.ps1" -ExtensionPath src\cfe\extname -ConfigPath src\cf -Mode A
|
python ".gemini/skills/cfe-diff/scripts/cfe-diff.py" -ExtensionPath src\cfe\extname -ConfigPath src\cf -Mode A
|
||||||
```
|
```
|
||||||
|
|
||||||
## Mode A — обзор расширения
|
## Mode A — обзор расширения
|
||||||
+4
-2
@@ -1,7 +1,8 @@
|
|||||||
# cfe-diff v1.2 — Analyze and compare 1C configuration extension (CFE) (+тип Bot; cfe-diff/cfe-borrow: недостающие типы)
|
# cfe-diff v1.4 — Analyze and compare 1C configuration extension (CFE)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
[CmdletBinding(PositionalBinding=$false)]
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory, Position=0)]
|
||||||
[string]$ExtensionPath,
|
[string]$ExtensionPath,
|
||||||
|
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -52,6 +53,7 @@ $childTypeDirMap = @{
|
|||||||
"CommonAttribute"="CommonAttributes"; "Style"="Styles"; "XDTOPackage"="XDTOPackages"
|
"CommonAttribute"="CommonAttributes"; "Style"="Styles"; "XDTOPackage"="XDTOPackages"
|
||||||
"WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
|
"WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
|
||||||
"Bot"="Bots"
|
"Bot"="Bots"
|
||||||
|
"PaletteColor"="PaletteColors"
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Parse extension Configuration.xml ---
|
# --- Parse extension Configuration.xml ---
|
||||||
+2
-1
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cfe-diff v1.2 — Analyze and compare 1C configuration extension (CFE) (+тип Bot; cfe-diff/cfe-borrow: недостающие типы)
|
# cfe-diff v1.4 — Analyze and compare 1C configuration extension (CFE)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -88,6 +88,7 @@ CHILD_TYPE_DIR_MAP = {
|
|||||||
"HTTPService": "HTTPServices",
|
"HTTPService": "HTTPServices",
|
||||||
"WSReference": "WSReferences",
|
"WSReference": "WSReferences",
|
||||||
"Bot": "Bots",
|
"Bot": "Bots",
|
||||||
|
"PaletteColor": "PaletteColors",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-init.ps1" -Name "МоёРасширение" -OutputDir "src\cfe\МоёРасширение" -ConfigPath "src\cf"
|
python ".gemini/skills/cfe-init/scripts/cfe-init.py" -Name "МоёРасширение" -OutputDir "src\cfe\МоёРасширение" -ConfigPath "src\cf"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
+2
-1
@@ -1,5 +1,6 @@
|
|||||||
# cfe-init v1.10 — Create 1C configuration extension scaffold (CFE) (+write_xml_file/write_utf8_bom: общий эталон записи)
|
# cfe-init v1.11 — Create 1C configuration extension scaffold (CFE) (+write_xml_file/write_utf8_bom: общий эталон записи)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
[CmdletBinding(PositionalBinding=$false)]
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
[string]$Name,
|
[string]$Name,
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cfe-init v1.10 — Create 1C configuration extension scaffold (CFE) (+write_xml_file/write_utf8_bom: общий эталон записи)
|
# cfe-init v1.11 — Create 1C configuration extension scaffold (CFE) (+write_xml_file/write_utf8_bom: общий эталон записи)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
"""Generates minimal XML source files for a 1C configuration extension."""
|
"""Generates minimal XML source files for a 1C configuration extension."""
|
||||||
import sys, os, re, argparse, uuid
|
import sys, os, re, argparse, uuid
|
||||||
@@ -88,7 +88,7 @@ allowed-tools:
|
|||||||
|
|
||||||
Правила:
|
Правила:
|
||||||
- Маркеры — на **отдельной строке с 0-й колонки** (без отступа), даже внутри отступов и текста запроса (`|…`).
|
- Маркеры — на **отдельной строке с 0-й колонки** (без отступа), даже внутри отступов и текста запроса (`|…`).
|
||||||
- **Незамеченные (unmarked) строки должны совпадать с оригиналом дословно** — это и есть «контроль». Если оригинал в конфигурации-источнике изменится, unmarked-контекст разойдётся → метод потребует актуализации (см. ниже; проверить пачкой — `-Check`).
|
- **Незамеченные (unmarked) строки должны совпадать с оригиналом дословно** — это и есть «контроль». Дословно — включая комментарии, регистр и пробелы внутри строки (`Х = Х + 1` ≠ `Х=Х+1`); свободны только отступ и пустые строки. Если оригинал в конфигурации-источнике изменится, unmarked-контекст разойдётся → метод потребует актуализации (см. ниже; проверить пачкой — `-Check`).
|
||||||
- Меняешь только свои `#Вставка`/`#Удаление`; чужой оригинал не трогай.
|
- Меняешь только свои `#Вставка`/`#Удаление`; чужой оригинал не трогай.
|
||||||
|
|
||||||
## Актуализация
|
## Актуализация
|
||||||
@@ -110,7 +110,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-patch-method.ps1" -ExtensionPath src\cfe\extname -ConfigPath src\cf -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
python ".gemini/skills/cfe-patch-method/scripts/cfe-patch-method.py" -ExtensionPath src\cfe\extname -ConfigPath src\cf -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
||||||
```
|
```
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
+128
-2
@@ -1,5 +1,6 @@
|
|||||||
# cfe-patch-method v2.7 — Source-aware method interceptor for 1C extension (CFE) (+прощающий ввод: имя каталога наравне с именем типа)
|
# cfe-patch-method v2.11 — Source-aware method interceptor for 1C extension (CFE)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
[CmdletBinding(PositionalBinding=$false)]
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
[string]$ExtensionPath,
|
[string]$ExtensionPath,
|
||||||
@@ -361,6 +362,22 @@ function Get-Normalized {
|
|||||||
return (($line -replace '\s+', ' ').Trim())
|
return (($line -replace '\s+', ' ').Trim())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Control comparison key, as the platform compares a &ИзменениеИКонтроль copy with the original:
|
||||||
|
# each line trimmed, blank lines dropped, everything else byte-for-byte and case-sensitive
|
||||||
|
# (inner spaces, comments and letter case are significant). Measured on 8.3.24 and 8.3.27.
|
||||||
|
function Get-ControlKey {
|
||||||
|
param($lines)
|
||||||
|
return (@($lines | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }) -join "`n")
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parameter count of a signature params text. The platform compares only the number of
|
||||||
|
# parameters of a &ИзменениеИКонтроль copy (names and default values it ignores).
|
||||||
|
function Get-ParamCount {
|
||||||
|
param([string]$paramsText)
|
||||||
|
if ([string]::IsNullOrWhiteSpace($paramsText)) { return 0 }
|
||||||
|
return @(Split-TopLevel $paramsText | Where-Object { $_.Trim() -ne '' }).Count
|
||||||
|
}
|
||||||
|
|
||||||
# Reconstruct v1 body and edit ops from a marked body
|
# Reconstruct v1 body and edit ops from a marked body
|
||||||
function Parse-MarkedBody {
|
function Parse-MarkedBody {
|
||||||
param($bodyLines)
|
param($bodyLines)
|
||||||
@@ -650,7 +667,14 @@ function Invoke-Resync {
|
|||||||
$v1norm = @($v1 | ForEach-Object { Get-Normalized $_ })
|
$v1norm = @($v1 | ForEach-Object { Get-Normalized $_ })
|
||||||
$v2norm = @($v2 | ForEach-Object { Get-Normalized $_ })
|
$v2norm = @($v2 | ForEach-Object { Get-Normalized $_ })
|
||||||
|
|
||||||
if (($v1norm -join "`n") -eq ($v2norm -join "`n")) {
|
# Signature: the platform rejects the interceptor when the parameter COUNT differs, so a
|
||||||
|
# body-only comparison would miss a vendor-added parameter. Names/defaults it ignores.
|
||||||
|
$extParamCount = Get-ParamCount $sig.ParamsText
|
||||||
|
$srcParamCount = Get-ParamCount $method.ParamsText
|
||||||
|
$paramsDrift = ($extParamCount -ne $srcParamCount)
|
||||||
|
$paramsReason = if ($paramsDrift) { "список параметров: в оригинале $srcParamCount, в перехватчике $extParamCount" } else { '' }
|
||||||
|
|
||||||
|
if (-not $paramsDrift -and [string]::Equals((Get-ControlKey $v1), (Get-ControlKey $v2), 'Ordinal')) {
|
||||||
return @{ Id = $methodId; Status = 'АКТУАЛЕН'; ExtBsl = $extBsl }
|
return @{ Id = $methodId; Status = 'АКТУАЛЕН'; ExtBsl = $extBsl }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -700,7 +724,9 @@ function Invoke-Resync {
|
|||||||
|
|
||||||
if ($ReportOnly) {
|
if ($ReportOnly) {
|
||||||
$st = if ($disputed.Count -gt 0) { 'КОНФЛИКТ' } elseif ($transferred.Count -eq 0 -and $absorbed.Count -gt 0) { 'ПЕРЕНЕСЕНО В ОСНОВНУЮ' } else { 'ДРЕЙФ' }
|
$st = if ($disputed.Count -gt 0) { 'КОНФЛИКТ' } elseif ($transferred.Count -eq 0 -and $absorbed.Count -gt 0) { 'ПЕРЕНЕСЕНО В ОСНОВНУЮ' } else { 'ДРЕЙФ' }
|
||||||
|
if ($paramsDrift -and $st -eq 'ПЕРЕНЕСЕНО В ОСНОВНУЮ') { $st = 'ДРЕЙФ' }
|
||||||
$rsn = if ($disputed.Count -gt 0) { Get-ResyncConflictReason $disputed } elseif ($st -eq 'ПЕРЕНЕСЕНО В ОСНОВНУЮ') { 'все правки уже в основной конфигурации' } else { '' }
|
$rsn = if ($disputed.Count -gt 0) { Get-ResyncConflictReason $disputed } elseif ($st -eq 'ПЕРЕНЕСЕНО В ОСНОВНУЮ') { 'все правки уже в основной конфигурации' } else { '' }
|
||||||
|
if ($paramsDrift) { $rsn = if ($rsn) { "$paramsReason; $rsn" } else { $paramsReason } }
|
||||||
return @{ Id = $methodId; Status = $st; ExtBsl = $extBsl; Transferred = $transferred.Count; Absorbed = $absorbed.Count; Disputed = $disputed.Count; Reason = $rsn; AbsorbedNotes = $absorbedNotes }
|
return @{ Id = $methodId; Status = $st; ExtBsl = $extBsl; Transferred = $transferred.Count; Absorbed = $absorbed.Count; Disputed = $disputed.Count; Reason = $rsn; AbsorbedNotes = $absorbedNotes }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -788,6 +814,100 @@ if (Test-Path $ExtensionPath -PathType Leaf) { $ExtensionPath = Split-Path $Exte
|
|||||||
$cfgFile = Join-Path $ExtensionPath "Configuration.xml"
|
$cfgFile = Join-Path $ExtensionPath "Configuration.xml"
|
||||||
if (-not (Test-Path $cfgFile)) { Write-Error "Configuration.xml не найден в расширении: $ExtensionPath"; exit 1 }
|
if (-not (Test-Path $cfgFile)) { Write-Error "Configuration.xml не найден в расширении: $ExtensionPath"; exit 1 }
|
||||||
|
|
||||||
|
# --- Пометка расширенного свойства (<xr:PropertyState>) ---
|
||||||
|
# Свойство появилось в формате 2.19 (8.3.26): на 2.18 и ниже платформа молча выбрасывает элемент
|
||||||
|
# при загрузке. С 2.19 Конфигуратор ставит его сам при выгрузке. Правило: флаг ставит тот, кто
|
||||||
|
# создал файл модуля, — здесь это мы. Имя свойства = базовое имя файла модуля.
|
||||||
|
# Копии этих функций есть в cfe-borrow (навыки автономны); держать их одинаковыми — сознательно.
|
||||||
|
function Get-FormatRank([string]$ver) {
|
||||||
|
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function Detect-FormatVersion([string]$dir) {
|
||||||
|
$d = $dir
|
||||||
|
while ($d) {
|
||||||
|
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
|
||||||
|
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
|
||||||
|
$extPath = "$d.xml"
|
||||||
|
if (Test-Path $extPath) {
|
||||||
|
$extText = [System.IO.File]::ReadAllText($extPath, [System.Text.Encoding]::UTF8)
|
||||||
|
$extHead = $extText.Substring(0, [Math]::Min(2000, $extText.Length))
|
||||||
|
if ($extHead -match '<(ExternalDataProcessor|ExternalReport)[ >]' -and $extHead -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
||||||
|
}
|
||||||
|
$cfgPath = Join-Path $d "Configuration.xml"
|
||||||
|
if (Test-Path $cfgPath) {
|
||||||
|
$cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
|
||||||
|
# Длину среза берём по СТРОКЕ, а не по размеру файла: размер в БАЙТАХ, Substring считает
|
||||||
|
# СИМВОЛЫ, и на кириллице байт больше — короткий Configuration.xml ронял навык исключением.
|
||||||
|
$head = $cfgText.Substring(0, [Math]::Min(2000, $cfgText.Length))
|
||||||
|
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
||||||
|
}
|
||||||
|
$parent = Split-Path $d -Parent
|
||||||
|
if ($parent -eq $d) { break }
|
||||||
|
$d = $parent
|
||||||
|
}
|
||||||
|
return "2.17"
|
||||||
|
}
|
||||||
|
|
||||||
|
function Build-PropertyStateXml {
|
||||||
|
param([string]$propertyName, [string]$indent)
|
||||||
|
|
||||||
|
$sb = New-Object System.Text.StringBuilder
|
||||||
|
$sb.AppendLine("${indent}<xr:PropertyState>") | Out-Null
|
||||||
|
$sb.AppendLine("${indent}`t<xr:Property>${propertyName}</xr:Property>") | Out-Null
|
||||||
|
$sb.AppendLine("${indent}`t<xr:State>Extended</xr:State>") | Out-Null
|
||||||
|
$sb.Append("${indent}</xr:PropertyState>") | Out-Null
|
||||||
|
return $sb.ToString()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Set-PropertyStateFlag {
|
||||||
|
param([string]$objFile, [string]$propertyName, [string]$formatVersion)
|
||||||
|
|
||||||
|
if ((Get-FormatRank $formatVersion) -lt 219) { return }
|
||||||
|
if (-not (Test-Path $objFile)) { return }
|
||||||
|
|
||||||
|
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||||
|
$text = [System.IO.File]::ReadAllText($objFile, $enc)
|
||||||
|
$nl = if ($text -match "`r`n") { "`r`n" } else { "`n" }
|
||||||
|
|
||||||
|
# ПЕРВЫЙ <InternalInfo> в файле — собственный у объекта: у реквизитов и подобъектов свои,
|
||||||
|
# но они лежат ниже, внутри <ChildObjects>.
|
||||||
|
$empty = [regex]::Match($text, '([ \t]*)<InternalInfo\s*/>')
|
||||||
|
$open = [regex]::Match($text, '(?s)([ \t]*)<InternalInfo>(.*?)</InternalInfo>')
|
||||||
|
|
||||||
|
if ($empty.Success -and (-not $open.Success -or $empty.Index -lt $open.Index)) {
|
||||||
|
$ind = $empty.Groups[1].Value
|
||||||
|
$block = Build-PropertyStateXml $propertyName ($ind + "`t")
|
||||||
|
$replacement = "${ind}<InternalInfo>${nl}${block}${nl}${ind}</InternalInfo>"
|
||||||
|
$text = $text.Remove($empty.Index, $empty.Length).Insert($empty.Index, $replacement)
|
||||||
|
} elseif ($open.Success) {
|
||||||
|
if ($open.Groups[2].Value -match "<xr:Property>$([regex]::Escape($propertyName))</xr:Property>") { return }
|
||||||
|
$ind = $open.Groups[1].Value
|
||||||
|
$block = Build-PropertyStateXml $propertyName ($ind + "`t")
|
||||||
|
# Дописываем в КОНЕЦ InternalInfo: у Конфигуратора PropertyState идёт после GeneratedType.
|
||||||
|
$closeAt = $open.Index + $open.Length - "</InternalInfo>".Length - $ind.Length
|
||||||
|
$text = $text.Insert($closeAt, "${block}${nl}")
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
[System.IO.File]::WriteAllText($objFile, $text, $enc)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Модуль формы сюда не попадает: у формы флаг называется Form и ставится при заимствовании,
|
||||||
|
# а не при появлении модуля (замер на 8.3.26 — пустой модуль формы платформа не выгружает).
|
||||||
|
function Get-ModuleFlagTarget {
|
||||||
|
param([string[]]$relParts, [string]$extRoot)
|
||||||
|
|
||||||
|
if ($relParts.Count -ne 4 -or $relParts[2] -ne "Ext") { return $null }
|
||||||
|
$prop = [System.IO.Path]::GetFileNameWithoutExtension($relParts[3])
|
||||||
|
return @{
|
||||||
|
File = (Join-Path (Join-Path $extRoot $relParts[0]) "$($relParts[1]).xml")
|
||||||
|
Property = $prop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# --- Read NamePrefix ---
|
# --- Read NamePrefix ---
|
||||||
$cfgDoc = New-Object System.Xml.XmlDocument
|
$cfgDoc = New-Object System.Xml.XmlDocument
|
||||||
$cfgDoc.PreserveWhitespace = $false
|
$cfgDoc.PreserveWhitespace = $false
|
||||||
@@ -1084,6 +1204,12 @@ if ($reuseRegionIdx -ge 0) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Модуль в расширении есть — отражаем это в метаданных объекта (формат ≥ 2.19).
|
||||||
|
$flagTarget = Get-ModuleFlagTarget $relParts $ExtensionPath
|
||||||
|
if ($flagTarget) {
|
||||||
|
Set-PropertyStateFlag $flagTarget.File $flagTarget.Property (Detect-FormatVersion $ExtensionPath)
|
||||||
|
}
|
||||||
|
|
||||||
Write-Host "[OK] Перехватчик &$decoratorRu(`"$MethodName`") — $placement"
|
Write-Host "[OK] Перехватчик &$decoratorRu(`"$MethodName`") — $placement"
|
||||||
Write-Host " Файл: $extBsl"
|
Write-Host " Файл: $extBsl"
|
||||||
Write-Host " Процедура: $interceptorName($($method.ParamsText -replace '\s+', ' '))"
|
Write-Host " Процедура: $interceptorName($($method.ParamsText -replace '\s+', ' '))"
|
||||||
+129
-3
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cfe-patch-method v2.7 — Source-aware method interceptor for 1C extension (CFE) (+прощающий ввод: имя каталога наравне с именем типа)
|
# cfe-patch-method v2.11 — Source-aware method interceptor for 1C extension (CFE)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -75,6 +75,99 @@ CONTEXT_RE = re.compile(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Пометка расширенного свойства (<xr:PropertyState>) ---
|
||||||
|
# Свойство появилось в формате 2.19 (8.3.26): на 2.18 и ниже платформа молча выбрасывает элемент
|
||||||
|
# при загрузке. С 2.19 Конфигуратор ставит его сам при выгрузке. Правило: флаг ставит тот, кто
|
||||||
|
# создал файл модуля, — здесь это мы. Имя свойства = базовое имя файла модуля.
|
||||||
|
# Копии этих функций есть в cfe-borrow (навыки автономны); держать их одинаковыми — сознательно.
|
||||||
|
def detect_format_version(d):
|
||||||
|
while d:
|
||||||
|
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
|
||||||
|
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
|
||||||
|
ext_path = d + ".xml"
|
||||||
|
if os.path.isfile(ext_path):
|
||||||
|
with open(ext_path, "r", encoding="utf-8-sig") as f:
|
||||||
|
ext_head = f.read(2000)
|
||||||
|
if re.search(r'<(ExternalDataProcessor|ExternalReport)[ >]', ext_head):
|
||||||
|
m = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', ext_head)
|
||||||
|
if m:
|
||||||
|
return m.group(1)
|
||||||
|
cfg_path = os.path.join(d, "Configuration.xml")
|
||||||
|
if os.path.isfile(cfg_path):
|
||||||
|
with open(cfg_path, "r", encoding="utf-8-sig") as f:
|
||||||
|
head = f.read(2000)
|
||||||
|
m = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', head)
|
||||||
|
if m:
|
||||||
|
return m.group(1)
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
break
|
||||||
|
d = parent
|
||||||
|
return "2.17"
|
||||||
|
|
||||||
|
|
||||||
|
def format_rank(ver):
|
||||||
|
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
|
||||||
|
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
|
||||||
|
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
|
||||||
|
|
||||||
|
|
||||||
|
def build_property_state_xml(property_name, indent):
|
||||||
|
return "\n".join([
|
||||||
|
f"{indent}<xr:PropertyState>",
|
||||||
|
f"{indent}\t<xr:Property>{property_name}</xr:Property>",
|
||||||
|
f"{indent}\t<xr:State>Extended</xr:State>",
|
||||||
|
f"{indent}</xr:PropertyState>",
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
def set_property_state_flag(obj_file, property_name, format_version):
|
||||||
|
if format_rank(format_version) < 219:
|
||||||
|
return
|
||||||
|
if not os.path.isfile(obj_file):
|
||||||
|
return
|
||||||
|
|
||||||
|
with open(obj_file, "r", encoding="utf-8-sig", newline="") as fh:
|
||||||
|
text = fh.read()
|
||||||
|
nl = "\r\n" if "\r\n" in text else "\n"
|
||||||
|
|
||||||
|
# ПЕРВЫЙ <InternalInfo> в файле — собственный у объекта: у реквизитов и подобъектов свои,
|
||||||
|
# но они лежат ниже, внутри <ChildObjects>.
|
||||||
|
empty = re.search(r"([ \t]*)<InternalInfo\s*/>", text)
|
||||||
|
opened = re.search(r"([ \t]*)<InternalInfo>(.*?)</InternalInfo>", text, re.S)
|
||||||
|
|
||||||
|
if empty and (not opened or empty.start() < opened.start()):
|
||||||
|
ind = empty.group(1)
|
||||||
|
block = build_property_state_xml(property_name, ind + "\t")
|
||||||
|
replacement = f"{ind}<InternalInfo>{nl}{block}{nl}{ind}</InternalInfo>"
|
||||||
|
text = text[:empty.start()] + replacement + text[empty.end():]
|
||||||
|
elif opened:
|
||||||
|
if re.search(rf"<xr:Property>{re.escape(property_name)}</xr:Property>", opened.group(2)):
|
||||||
|
return
|
||||||
|
ind = opened.group(1)
|
||||||
|
block = build_property_state_xml(property_name, ind + "\t")
|
||||||
|
# Дописываем в КОНЕЦ InternalInfo: у Конфигуратора PropertyState идёт после GeneratedType.
|
||||||
|
close_at = opened.end() - len("</InternalInfo>") - len(ind)
|
||||||
|
text = text[:close_at] + block + nl + text[close_at:]
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
with open(obj_file, "w", encoding="utf-8-sig", newline="") as fh:
|
||||||
|
fh.write(text)
|
||||||
|
|
||||||
|
|
||||||
|
# Модуль формы сюда не попадает: у формы флаг называется Form и ставится при заимствовании,
|
||||||
|
# а не при появлении модуля (замер на 8.3.26 — пустой модуль формы платформа не выгружает).
|
||||||
|
def get_module_flag_target(rel_parts, ext_root):
|
||||||
|
if len(rel_parts) != 4 or rel_parts[2] != "Ext":
|
||||||
|
return None
|
||||||
|
prop = os.path.splitext(rel_parts[3])[0]
|
||||||
|
return {
|
||||||
|
"file": os.path.join(ext_root, rel_parts[0], f"{rel_parts[1]}.xml"),
|
||||||
|
"property": prop,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_module_rel_path(module_path):
|
def get_module_rel_path(module_path):
|
||||||
parts = module_path.split(".")
|
parts = module_path.split(".")
|
||||||
if len(parts) < 2:
|
if len(parts) < 2:
|
||||||
@@ -398,6 +491,21 @@ def normalize(line):
|
|||||||
return re.sub(r'\s+', ' ', line).strip()
|
return re.sub(r'\s+', ' ', line).strip()
|
||||||
|
|
||||||
|
|
||||||
|
# Control comparison key, as the platform compares a &ИзменениеИКонтроль copy with the original:
|
||||||
|
# each line trimmed, blank lines dropped, everything else byte-for-byte and case-sensitive
|
||||||
|
# (inner spaces, comments and letter case are significant). Measured on 8.3.24 and 8.3.27.
|
||||||
|
def control_key(lines):
|
||||||
|
return "\n".join([k for k in (x.strip() for x in lines) if k != ""])
|
||||||
|
|
||||||
|
|
||||||
|
# Parameter count of a signature params text. The platform compares only the number of
|
||||||
|
# parameters of a &ИзменениеИКонтроль copy (names and default values it ignores).
|
||||||
|
def param_count(params_text):
|
||||||
|
if not params_text or not params_text.strip():
|
||||||
|
return 0
|
||||||
|
return len([p for p in split_top_level(params_text) if p.strip()])
|
||||||
|
|
||||||
|
|
||||||
def parse_marked_body(body_lines):
|
def parse_marked_body(body_lines):
|
||||||
v1 = []
|
v1 = []
|
||||||
ops = []
|
ops = []
|
||||||
@@ -841,6 +949,12 @@ def main():
|
|||||||
|
|
||||||
place_new(ext_bsl, ext_lines, ext_exists, method["chain"], core)
|
place_new(ext_bsl, ext_lines, ext_exists, method["chain"], core)
|
||||||
|
|
||||||
|
# Модуль в расширении есть — отражаем это в метаданных объекта (формат ≥ 2.19).
|
||||||
|
flag_target = get_module_flag_target(rel_parts, extension_path)
|
||||||
|
if flag_target:
|
||||||
|
set_property_state_flag(flag_target["file"], flag_target["property"],
|
||||||
|
detect_format_version(extension_path))
|
||||||
|
|
||||||
# emit summary
|
# emit summary
|
||||||
placement = place_new.placement
|
placement = place_new.placement
|
||||||
print('[OK] Перехватчик &%s("%s") — %s' % (decorator_ru, method_name, placement))
|
print('[OK] Перехватчик &%s("%s") — %s' % (decorator_ru, method_name, placement))
|
||||||
@@ -1030,7 +1144,7 @@ def resync_one(ext_bsl, ext_lines, dup, method, logical_module, conflict_folder,
|
|||||||
sig = read_signature(ext_lines, sig_line_idx)
|
sig = read_signature(ext_lines, sig_line_idx)
|
||||||
if not sig:
|
if not sig:
|
||||||
return {"id": method_id, "status": "ОШИБКА", "ext_bsl": ext_bsl, "reason": "не разобрать сигнатуру"}
|
return {"id": method_id, "status": "ОШИБКА", "ext_bsl": ext_bsl, "reason": "не разобрать сигнатуру"}
|
||||||
_params, sig_end = sig
|
ext_params_text, sig_end = sig
|
||||||
is_func = bool(re.match(r'^\s*(?:Асинх\s+)?Функция\b', ext_lines[sig_line_idx], re.IGNORECASE))
|
is_func = bool(re.match(r'^\s*(?:Асинх\s+)?Функция\b', ext_lines[sig_line_idx], re.IGNORECASE))
|
||||||
end_re = re.compile(r'^\s*КонецФункции\b' if is_func else r'^\s*КонецПроцедуры\b', re.IGNORECASE)
|
end_re = re.compile(r'^\s*КонецФункции\b' if is_func else r'^\s*КонецПроцедуры\b', re.IGNORECASE)
|
||||||
block_end = -1
|
block_end = -1
|
||||||
@@ -1047,7 +1161,15 @@ def resync_one(ext_bsl, ext_lines, dup, method, logical_module, conflict_folder,
|
|||||||
v1norm = [normalize(x) for x in v1]
|
v1norm = [normalize(x) for x in v1]
|
||||||
v2norm = [normalize(x) for x in v2]
|
v2norm = [normalize(x) for x in v2]
|
||||||
|
|
||||||
if "\n".join(v1norm) == "\n".join(v2norm):
|
# Signature: the platform rejects the interceptor when the parameter COUNT differs, so a
|
||||||
|
# body-only comparison would miss a vendor-added parameter. Names/defaults it ignores.
|
||||||
|
ext_param_count = param_count(ext_params_text)
|
||||||
|
src_param_count = param_count(method["params_text"])
|
||||||
|
params_drift = ext_param_count != src_param_count
|
||||||
|
params_reason = ("список параметров: в оригинале %d, в перехватчике %d"
|
||||||
|
% (src_param_count, ext_param_count)) if params_drift else ""
|
||||||
|
|
||||||
|
if not params_drift and control_key(v1) == control_key(v2):
|
||||||
return {"id": method_id, "status": "АКТУАЛЕН", "ext_bsl": ext_bsl}
|
return {"id": method_id, "status": "АКТУАЛЕН", "ext_bsl": ext_bsl}
|
||||||
|
|
||||||
insert_top = []; insert_after = {}; del_start = set(); del_end = set(); disputed = []; transferred = 0; absorbed = 0; absorbed_notes = []
|
insert_top = []; insert_after = {}; del_start = set(); del_end = set(); disputed = []; transferred = 0; absorbed = 0; absorbed_notes = []
|
||||||
@@ -1109,7 +1231,11 @@ def resync_one(ext_bsl, ext_lines, dup, method, logical_module, conflict_folder,
|
|||||||
st = "ПЕРЕНЕСЕНО В ОСНОВНУЮ"
|
st = "ПЕРЕНЕСЕНО В ОСНОВНУЮ"
|
||||||
else:
|
else:
|
||||||
st = "ДРЕЙФ"
|
st = "ДРЕЙФ"
|
||||||
|
if params_drift and st == "ПЕРЕНЕСЕНО В ОСНОВНУЮ":
|
||||||
|
st = "ДРЕЙФ"
|
||||||
rsn = conflict_reason(disputed) if disputed else ("все правки уже в основной конфигурации" if st == "ПЕРЕНЕСЕНО В ОСНОВНУЮ" else "")
|
rsn = conflict_reason(disputed) if disputed else ("все правки уже в основной конфигурации" if st == "ПЕРЕНЕСЕНО В ОСНОВНУЮ" else "")
|
||||||
|
if params_drift:
|
||||||
|
rsn = ("%s; %s" % (params_reason, rsn)) if rsn else params_reason
|
||||||
return {"id": method_id, "status": st, "ext_bsl": ext_bsl, "transferred": transferred,
|
return {"id": method_id, "status": st, "ext_bsl": ext_bsl, "transferred": transferred,
|
||||||
"absorbed": absorbed, "disputed": len(disputed), "reason": rsn, "absorbed_notes": absorbed_notes}
|
"absorbed": absorbed, "disputed": len(disputed), "reason": rsn, "absorbed_notes": absorbed_notes}
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ allowed-tools:
|
|||||||
|
|
||||||
# /cfe-validate — валидация расширения конфигурации (CFE)
|
# /cfe-validate — валидация расширения конфигурации (CFE)
|
||||||
|
|
||||||
Проверяет структурную корректность расширения: XML-формат, свойства, состав, заимствованные объекты. Аналог `/cf-validate`, но для расширений.
|
Проверяет структурную корректность расширения: XML-формат, свойства, состав, заимствованные объекты, права ролей. Аналог `/cf-validate`, но для расширений.
|
||||||
|
|
||||||
## Параметры
|
## Параметры
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src\cfe\extname"
|
python ".gemini/skills/cfe-validate/scripts/cfe-validate.py" -ExtensionPath "src\cfe\extname"
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src\cfe\extname\Configuration.xml"
|
python ".gemini/skills/cfe-validate/scripts/cfe-validate.py" -ExtensionPath "src\cfe\extname\Configuration.xml"
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src\cfe\extname" -ConfigPath "src\cf"
|
python ".gemini/skills/cfe-validate/scripts/cfe-validate.py" -ExtensionPath "src\cfe\extname" -ConfigPath "src\cf"
|
||||||
```
|
```
|
||||||
+134
-7
@@ -1,7 +1,8 @@
|
|||||||
# cfe-validate v1.10 — Validate 1C configuration extension structure (CFE) (полнота GeneratedType, ТЧ из AdditionalColumns, сверка путей с -ConfigPath)
|
# cfe-validate v1.15 — Validate 1C configuration extension structure (CFE)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
[CmdletBinding(PositionalBinding=$false)]
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory, Position=0)]
|
||||||
[Alias('Path')]
|
[Alias('Path')]
|
||||||
[string]$ExtensionPath,
|
[string]$ExtensionPath,
|
||||||
|
|
||||||
@@ -107,7 +108,28 @@ function Get-FormatRank([string]$ver) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- Reference tables ---
|
# --- Reference tables ---
|
||||||
$guidPattern = '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
|
# Модули заимствованных объектов: тип → виды модулей. Имя свойства в <xr:PropertyState>
|
||||||
|
# совпадает с базовым именем файла модуля. Копия таблицы есть в cfe-borrow (навыки автономны).
|
||||||
|
$moduleKindsByType = @{
|
||||||
|
"CommonModule"=@("Module"); "HTTPService"=@("Module"); "WebService"=@("Module")
|
||||||
|
"Catalog"=@("ObjectModule","ManagerModule"); "Document"=@("ObjectModule","ManagerModule")
|
||||||
|
"Report"=@("ObjectModule","ManagerModule"); "DataProcessor"=@("ObjectModule","ManagerModule")
|
||||||
|
"ExchangePlan"=@("ObjectModule","ManagerModule")
|
||||||
|
"ChartOfCharacteristicTypes"=@("ObjectModule","ManagerModule")
|
||||||
|
"ChartOfAccounts"=@("ObjectModule","ManagerModule")
|
||||||
|
"ChartOfCalculationTypes"=@("ObjectModule","ManagerModule")
|
||||||
|
"BusinessProcess"=@("ObjectModule","ManagerModule"); "Task"=@("ObjectModule","ManagerModule")
|
||||||
|
"InformationRegister"=@("RecordSetModule","ManagerModule")
|
||||||
|
"AccumulationRegister"=@("RecordSetModule","ManagerModule")
|
||||||
|
"AccountingRegister"=@("RecordSetModule","ManagerModule")
|
||||||
|
"CalculationRegister"=@("RecordSetModule","ManagerModule")
|
||||||
|
"Sequence"=@("RecordSetModule","ManagerModule")
|
||||||
|
"Constant"=@("ValueManagerModule","ManagerModule")
|
||||||
|
"Enum"=@("ManagerModule"); "DocumentJournal"=@("ManagerModule")
|
||||||
|
"FilterCriterion"=@("ManagerModule")
|
||||||
|
}
|
||||||
|
|
||||||
|
$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_]*$'
|
$identPattern = '^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_][A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$'
|
||||||
|
|
||||||
# 7 fixed ClassIds for Configuration
|
# 7 fixed ClassIds for Configuration
|
||||||
@@ -121,14 +143,14 @@ $validClassIds = @(
|
|||||||
"fb282519-d103-4dd3-bc12-cb271d631dfc"
|
"fb282519-d103-4dd3-bc12-cb271d631dfc"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 44 types in canonical order
|
# 46 types in canonical order
|
||||||
$childObjectTypes = @(
|
$childObjectTypes = @(
|
||||||
"Language","Subsystem","StyleItem","Style",
|
"Language","Subsystem","StyleItem","Style",
|
||||||
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
||||||
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
|
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
|
||||||
"XDTOPackage","WebService","HTTPService","WSReference",
|
"XDTOPackage","WebService","HTTPService","WSReference",
|
||||||
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
||||||
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
"FunctionalOptionsParameter","DefinedType","Bot","PaletteColor","CommonCommand","CommandGroup",
|
||||||
"Constant","CommonForm","Catalog","Document",
|
"Constant","CommonForm","Catalog","Document",
|
||||||
"DocumentNumerator","Sequence","DocumentJournal","Enum",
|
"DocumentNumerator","Sequence","DocumentJournal","Enum",
|
||||||
"Report","DataProcessor","InformationRegister","AccumulationRegister",
|
"Report","DataProcessor","InformationRegister","AccumulationRegister",
|
||||||
@@ -139,7 +161,7 @@ $childObjectTypes = @(
|
|||||||
|
|
||||||
# Type -> directory mapping
|
# Type -> directory mapping
|
||||||
$childTypeDirMap = @{
|
$childTypeDirMap = @{
|
||||||
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"; "Bot"="Bots"
|
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"; "Bot"="Bots"; "PaletteColor"="PaletteColors"
|
||||||
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"
|
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"
|
||||||
"CommonTemplate"="CommonTemplates"; "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"
|
"CommonTemplate"="CommonTemplates"; "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"
|
||||||
"CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"; "XDTOPackage"="XDTOPackages"
|
"CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"; "XDTOPackage"="XDTOPackages"
|
||||||
@@ -1169,8 +1191,113 @@ if (-not $script:stopped -and $script:borrowedFormsWithTree.Count -gt 0) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Check 15: основные роли расширения не дают прав на заимствованные объекты ---
|
||||||
|
# Платформа: «Назначение прав доступа на заимствованные объекты основными ролями в
|
||||||
|
# расширениях недопустимо». Роль вне <DefaultRoles> так делать вправе — проверяем только
|
||||||
|
# основные. Ловится статически, а по симптому (отказ загрузки) причина не читается.
|
||||||
|
$defaultRoleNodes = @($cfgNode.SelectNodes("md:Properties/md:DefaultRoles/xr:Item", $ns))
|
||||||
|
if ($defaultRoleNodes.Count -gt 0) {
|
||||||
|
$adoptedCache = @{}
|
||||||
|
|
||||||
|
function Test-ObjectAdopted {
|
||||||
|
param([string]$typeName, [string]$objName)
|
||||||
|
$key = "$typeName.$objName"
|
||||||
|
if ($adoptedCache.ContainsKey($key)) { return $adoptedCache[$key] }
|
||||||
|
$adoptedCache[$key] = $false
|
||||||
|
if ($childTypeDirMap.ContainsKey($typeName)) {
|
||||||
|
$objPath = Join-Path (Join-Path $configDir $childTypeDirMap[$typeName]) "$objName.xml"
|
||||||
|
if (Test-Path $objPath) {
|
||||||
|
try {
|
||||||
|
$objDoc = New-Object System.Xml.XmlDocument
|
||||||
|
$objDoc.Load($objPath)
|
||||||
|
$objNs = New-Object System.Xml.XmlNamespaceManager($objDoc.NameTable)
|
||||||
|
$objNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||||
|
$ob = $objDoc.SelectSingleNode("/md:MetaDataObject/md:$typeName/md:Properties/md:ObjectBelonging", $objNs)
|
||||||
|
if ($ob -and $ob.InnerText -eq "Adopted") { $adoptedCache[$key] = $true }
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $adoptedCache[$key]
|
||||||
|
}
|
||||||
|
|
||||||
|
$check15Ok = $true
|
||||||
|
$check15Count = 0
|
||||||
|
foreach ($rn in $defaultRoleNodes) {
|
||||||
|
$roleRef = $rn.InnerText
|
||||||
|
if ($roleRef -notmatch '^Role\.(.+)$') { continue }
|
||||||
|
$defRoleName = $Matches[1]
|
||||||
|
$rightsPath = Join-Path (Join-Path (Join-Path $configDir "Roles") $defRoleName) "Ext\Rights.xml"
|
||||||
|
if (-not (Test-Path $rightsPath)) { continue }
|
||||||
|
try {
|
||||||
|
$rDoc = New-Object System.Xml.XmlDocument
|
||||||
|
$rDoc.Load($rightsPath)
|
||||||
|
} catch {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
$rNs = New-Object System.Xml.XmlNamespaceManager($rDoc.NameTable)
|
||||||
|
$rNs.AddNamespace("r", "http://v8.1c.ru/8.2/roles")
|
||||||
|
foreach ($nameNode in $rDoc.SelectNodes("/r:Rights/r:object/r:name", $rNs)) {
|
||||||
|
$fullName = $nameNode.InnerText
|
||||||
|
$segs = $fullName.Split(".")
|
||||||
|
# Configuration.* — права самого расширения, не объект; заимствования там нет.
|
||||||
|
if ($segs.Count -lt 2 -or $segs[0] -eq "Configuration") { continue }
|
||||||
|
$check15Count++
|
||||||
|
if (Test-ObjectAdopted $segs[0] $segs[1]) {
|
||||||
|
Report-Error ("15. Роль '$defRoleName' входит в DefaultRoles и даёт права на заимствованный $($segs[0]).$($segs[1]) " +
|
||||||
|
"($fullName): платформа это запрещает. Вынесите такие права в отдельную роль вне DefaultRoles.")
|
||||||
|
$check15Ok = $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($check15Ok -and $check15Count -gt 0) {
|
||||||
|
Report-OK "15. Основные роли: прав на заимствованные объекты нет ($check15Count checked)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($script:stopped) { & $finalize; exit 1 }
|
if ($script:stopped) { & $finalize; exit 1 }
|
||||||
|
|
||||||
|
# --- Check 16: модуль заимствованного объекта и пометка расширенного свойства ---
|
||||||
|
# Свойство <xr:PropertyState> появилось в формате 2.19 (8.3.26); ниже платформа его молча
|
||||||
|
# выбрасывает, поэтому там проверять нечего. С 2.19 состояние обязано соответствовать факту:
|
||||||
|
# есть файл модуля — есть пометка, и наоборот. Перекос платформа принимает (проверено на стенде),
|
||||||
|
# но выгрузка Конфигуратора так не выглядит — отсюда предупреждение, а не ошибка.
|
||||||
|
if ($versionRank -ge 219 -and $childObjNode) {
|
||||||
|
$stateIssues = @()
|
||||||
|
$stateChecked = 0
|
||||||
|
foreach ($child in $childObjNode.ChildNodes) {
|
||||||
|
if ($child.NodeType -ne 'Element') { continue }
|
||||||
|
$typeName = $child.LocalName
|
||||||
|
if (-not $moduleKindsByType.ContainsKey($typeName)) { continue }
|
||||||
|
if (-not $childTypeDirMap.ContainsKey($typeName)) { continue }
|
||||||
|
$stateObjName = $child.InnerText.Trim()
|
||||||
|
if (-not $stateObjName) { continue }
|
||||||
|
$typeDir = Join-Path $configDir $childTypeDirMap[$typeName]
|
||||||
|
$objFile = Join-Path $typeDir "$stateObjName.xml"
|
||||||
|
if (-not (Test-Path $objFile)) { continue }
|
||||||
|
$objText = [System.IO.File]::ReadAllText($objFile, [System.Text.Encoding]::UTF8)
|
||||||
|
if ($objText -notmatch '<ObjectBelonging>Adopted</ObjectBelonging>') { continue }
|
||||||
|
|
||||||
|
foreach ($kind in $moduleKindsByType[$typeName]) {
|
||||||
|
$stateChecked++
|
||||||
|
$hasFile = Test-Path (Join-Path (Join-Path (Join-Path $typeDir $stateObjName) "Ext") "$kind.bsl")
|
||||||
|
$hasFlag = $objText -match "<xr:Property>$kind</xr:Property>"
|
||||||
|
if ($hasFile -and -not $hasFlag) {
|
||||||
|
$stateIssues += "$typeName.$stateObjName — есть $kind.bsl, но нет <xr:PropertyState> для $kind"
|
||||||
|
} elseif ($hasFlag -and -not $hasFile) {
|
||||||
|
$stateIssues += "$typeName.$stateObjName — есть <xr:PropertyState> для $kind, но нет $kind.bsl"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($stateChecked -gt 0) {
|
||||||
|
if ($stateIssues.Count -eq 0) {
|
||||||
|
Report-OK "16. Модули заимствованных объектов: пометки расширенных свойств согласованы ($stateChecked)"
|
||||||
|
} else {
|
||||||
|
foreach ($issue in $stateIssues) { Report-Warn "16. $issue" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# --- Breadcrumb: controlled methods (&ИзменениеИКонтроль) drift is not checked here ---
|
# --- Breadcrumb: controlled methods (&ИзменениеИКонтроль) drift is not checked here ---
|
||||||
$extRootDir = Split-Path $resolvedPath -Parent
|
$extRootDir = Split-Path $resolvedPath -Parent
|
||||||
$ctrlCount = 0
|
$ctrlCount = 0
|
||||||
+124
-5
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cfe-validate v1.10 — Validate 1C configuration extension XML structure (CFE) (полнота GeneratedType, ТЧ из AdditionalColumns, сверка путей с -ConfigPath)
|
# cfe-validate v1.15 — Validate 1C configuration extension XML structure (CFE)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
"""Validates extension Configuration.xml: root, InternalInfo, extension properties, ChildObjects, borrowed objects."""
|
"""Validates extension Configuration.xml: root, InternalInfo, extension properties, ChildObjects, borrowed objects."""
|
||||||
import sys, os, argparse, re
|
import sys, os, argparse, re
|
||||||
@@ -55,14 +55,14 @@ VALID_CLASS_IDS = [
|
|||||||
'fb282519-d103-4dd3-bc12-cb271d631dfc',
|
'fb282519-d103-4dd3-bc12-cb271d631dfc',
|
||||||
]
|
]
|
||||||
|
|
||||||
# 44 types in canonical order
|
# 46 types in canonical order
|
||||||
CHILD_OBJECT_TYPES = [
|
CHILD_OBJECT_TYPES = [
|
||||||
'Language', 'Subsystem', 'StyleItem', 'Style',
|
'Language', 'Subsystem', 'StyleItem', 'Style',
|
||||||
'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate',
|
'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate',
|
||||||
'FilterCriterion', 'CommonModule', 'Bot', 'CommonAttribute', 'ExchangePlan',
|
'FilterCriterion', 'CommonModule', 'CommonAttribute', 'ExchangePlan',
|
||||||
'XDTOPackage', 'WebService', 'HTTPService', 'WSReference',
|
'XDTOPackage', 'WebService', 'HTTPService', 'WSReference',
|
||||||
'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption',
|
'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption',
|
||||||
'FunctionalOptionsParameter', 'DefinedType', 'CommonCommand', 'CommandGroup',
|
'FunctionalOptionsParameter', 'DefinedType', 'Bot', 'PaletteColor', 'CommonCommand', 'CommandGroup',
|
||||||
'Constant', 'CommonForm', 'Catalog', 'Document',
|
'Constant', 'CommonForm', 'Catalog', 'Document',
|
||||||
'DocumentNumerator', 'Sequence', 'DocumentJournal', 'Enum',
|
'DocumentNumerator', 'Sequence', 'DocumentJournal', 'Enum',
|
||||||
'Report', 'DataProcessor', 'InformationRegister', 'AccumulationRegister',
|
'Report', 'DataProcessor', 'InformationRegister', 'AccumulationRegister',
|
||||||
@@ -71,12 +71,33 @@ CHILD_OBJECT_TYPES = [
|
|||||||
'BusinessProcess', 'Task', 'IntegrationService',
|
'BusinessProcess', 'Task', 'IntegrationService',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Модули заимствованных объектов: тип → виды модулей. Имя свойства в <xr:PropertyState>
|
||||||
|
# совпадает с базовым именем файла модуля. Копия таблицы есть в cfe-borrow (навыки автономны).
|
||||||
|
MODULE_KINDS_BY_TYPE = {
|
||||||
|
"CommonModule": ["Module"], "HTTPService": ["Module"], "WebService": ["Module"],
|
||||||
|
"Catalog": ["ObjectModule", "ManagerModule"], "Document": ["ObjectModule", "ManagerModule"],
|
||||||
|
"Report": ["ObjectModule", "ManagerModule"], "DataProcessor": ["ObjectModule", "ManagerModule"],
|
||||||
|
"ExchangePlan": ["ObjectModule", "ManagerModule"],
|
||||||
|
"ChartOfCharacteristicTypes": ["ObjectModule", "ManagerModule"],
|
||||||
|
"ChartOfAccounts": ["ObjectModule", "ManagerModule"],
|
||||||
|
"ChartOfCalculationTypes": ["ObjectModule", "ManagerModule"],
|
||||||
|
"BusinessProcess": ["ObjectModule", "ManagerModule"], "Task": ["ObjectModule", "ManagerModule"],
|
||||||
|
"InformationRegister": ["RecordSetModule", "ManagerModule"],
|
||||||
|
"AccumulationRegister": ["RecordSetModule", "ManagerModule"],
|
||||||
|
"AccountingRegister": ["RecordSetModule", "ManagerModule"],
|
||||||
|
"CalculationRegister": ["RecordSetModule", "ManagerModule"],
|
||||||
|
"Sequence": ["RecordSetModule", "ManagerModule"],
|
||||||
|
"Constant": ["ValueManagerModule", "ManagerModule"],
|
||||||
|
"Enum": ["ManagerModule"], "DocumentJournal": ["ManagerModule"],
|
||||||
|
"FilterCriterion": ["ManagerModule"],
|
||||||
|
}
|
||||||
|
|
||||||
# Type -> directory mapping
|
# Type -> directory mapping
|
||||||
CHILD_TYPE_DIR_MAP = {
|
CHILD_TYPE_DIR_MAP = {
|
||||||
'Language': 'Languages', 'Subsystem': 'Subsystems', 'StyleItem': 'StyleItems', 'Style': 'Styles',
|
'Language': 'Languages', 'Subsystem': 'Subsystems', 'StyleItem': 'StyleItems', 'Style': 'Styles',
|
||||||
'CommonPicture': 'CommonPictures', 'SessionParameter': 'SessionParameters', 'Role': 'Roles',
|
'CommonPicture': 'CommonPictures', 'SessionParameter': 'SessionParameters', 'Role': 'Roles',
|
||||||
'CommonTemplate': 'CommonTemplates', 'FilterCriterion': 'FilterCriteria', 'CommonModule': 'CommonModules',
|
'CommonTemplate': 'CommonTemplates', 'FilterCriterion': 'FilterCriteria', 'CommonModule': 'CommonModules',
|
||||||
'Bot': 'Bots',
|
'Bot': 'Bots', 'PaletteColor': 'PaletteColors',
|
||||||
'CommonAttribute': 'CommonAttributes', 'ExchangePlan': 'ExchangePlans', 'XDTOPackage': 'XDTOPackages',
|
'CommonAttribute': 'CommonAttributes', 'ExchangePlan': 'ExchangePlans', 'XDTOPackage': 'XDTOPackages',
|
||||||
'WebService': 'WebServices', 'HTTPService': 'HTTPServices', 'WSReference': 'WSReferences',
|
'WebService': 'WebServices', 'HTTPService': 'HTTPServices', 'WSReference': 'WSReferences',
|
||||||
'EventSubscription': 'EventSubscriptions', 'ScheduledJob': 'ScheduledJobs',
|
'EventSubscription': 'EventSubscriptions', 'ScheduledJob': 'ScheduledJobs',
|
||||||
@@ -1141,10 +1162,108 @@ def main():
|
|||||||
if check14_ok:
|
if check14_ok:
|
||||||
r.ok(f'14. Object paths vs source config: {path_check_count} checked')
|
r.ok(f'14. Object paths vs source config: {path_check_count} checked')
|
||||||
|
|
||||||
|
# --- Check 15: основные роли расширения не дают прав на заимствованные объекты ---
|
||||||
|
# Платформа: «Назначение прав доступа на заимствованные объекты основными ролями в
|
||||||
|
# расширениях недопустимо». Роль вне <DefaultRoles> так делать вправе — проверяем только
|
||||||
|
# основные. Ловится статически, а по симптому (отказ загрузки) причина не читается.
|
||||||
|
default_role_nodes = cfg_node.findall('md:Properties/md:DefaultRoles/xr:Item', NS)
|
||||||
|
if default_role_nodes:
|
||||||
|
adopted_cache = {}
|
||||||
|
|
||||||
|
def is_object_adopted(type_name, obj_name):
|
||||||
|
key = f"{type_name}.{obj_name}"
|
||||||
|
if key in adopted_cache:
|
||||||
|
return adopted_cache[key]
|
||||||
|
adopted_cache[key] = False
|
||||||
|
dir_name = CHILD_TYPE_DIR_MAP.get(type_name)
|
||||||
|
if dir_name:
|
||||||
|
obj_path = os.path.join(config_dir, dir_name, obj_name + '.xml')
|
||||||
|
if os.path.isfile(obj_path):
|
||||||
|
try:
|
||||||
|
obj_root = etree.parse(obj_path).getroot()
|
||||||
|
ob = obj_root.find(f'md:{type_name}/md:Properties/md:ObjectBelonging', NS)
|
||||||
|
if ob is not None and (ob.text or '') == 'Adopted':
|
||||||
|
adopted_cache[key] = True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return adopted_cache[key]
|
||||||
|
|
||||||
|
check15_ok = True
|
||||||
|
check15_count = 0
|
||||||
|
roles_ns = {'r': 'http://v8.1c.ru/8.2/roles'}
|
||||||
|
for rn in default_role_nodes:
|
||||||
|
m = re.match(r'^Role\.(.+)$', rn.text or '')
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
def_role_name = m.group(1)
|
||||||
|
rights_path = os.path.join(config_dir, 'Roles', def_role_name, 'Ext', 'Rights.xml')
|
||||||
|
if not os.path.isfile(rights_path):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
rights_root = etree.parse(rights_path).getroot()
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
for name_node in rights_root.findall('r:object/r:name', roles_ns):
|
||||||
|
full_name = name_node.text or ''
|
||||||
|
segs = full_name.split('.')
|
||||||
|
# Configuration.* — права самого расширения, не объект; заимствования там нет.
|
||||||
|
if len(segs) < 2 or segs[0] == 'Configuration':
|
||||||
|
continue
|
||||||
|
check15_count += 1
|
||||||
|
if is_object_adopted(segs[0], segs[1]):
|
||||||
|
r.error(f"15. Роль '{def_role_name}' входит в DefaultRoles и даёт права на заимствованный "
|
||||||
|
f"{segs[0]}.{segs[1]} ({full_name}): платформа это запрещает. "
|
||||||
|
"Вынесите такие права в отдельную роль вне DefaultRoles.")
|
||||||
|
check15_ok = False
|
||||||
|
if check15_ok and check15_count > 0:
|
||||||
|
r.ok(f'15. Основные роли: прав на заимствованные объекты нет ({check15_count} checked)')
|
||||||
|
|
||||||
if r.stopped:
|
if r.stopped:
|
||||||
r.finalize(out_file)
|
r.finalize(out_file)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
# --- Check 16: модуль заимствованного объекта и пометка расширенного свойства ---
|
||||||
|
# Свойство <xr:PropertyState> появилось в формате 2.19 (8.3.26); ниже платформа его молча
|
||||||
|
# выбрасывает, поэтому там проверять нечего. С 2.19 состояние обязано соответствовать факту:
|
||||||
|
# есть файл модуля — есть пометка, и наоборот. Перекос платформа принимает (проверено на
|
||||||
|
# стенде), но выгрузка Конфигуратора так не выглядит — отсюда предупреждение, а не ошибка.
|
||||||
|
if version_rank >= 219 and child_obj_node is not None:
|
||||||
|
state_issues = []
|
||||||
|
state_checked = 0
|
||||||
|
for child in child_obj_node:
|
||||||
|
if not isinstance(child.tag, str):
|
||||||
|
continue
|
||||||
|
type_name = etree.QName(child.tag).localname
|
||||||
|
if type_name not in MODULE_KINDS_BY_TYPE or type_name not in CHILD_TYPE_DIR_MAP:
|
||||||
|
continue
|
||||||
|
obj_name_val = (child.text or '').strip()
|
||||||
|
if not obj_name_val:
|
||||||
|
continue
|
||||||
|
type_dir = os.path.join(config_dir, CHILD_TYPE_DIR_MAP[type_name])
|
||||||
|
obj_file = os.path.join(type_dir, f'{obj_name_val}.xml')
|
||||||
|
if not os.path.isfile(obj_file):
|
||||||
|
continue
|
||||||
|
with open(obj_file, 'r', encoding='utf-8-sig') as f:
|
||||||
|
obj_text = f.read()
|
||||||
|
if '<ObjectBelonging>Adopted</ObjectBelonging>' not in obj_text:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for kind in MODULE_KINDS_BY_TYPE[type_name]:
|
||||||
|
state_checked += 1
|
||||||
|
has_file = os.path.isfile(os.path.join(type_dir, obj_name_val, 'Ext', f'{kind}.bsl'))
|
||||||
|
has_flag = f'<xr:Property>{kind}</xr:Property>' in obj_text
|
||||||
|
if has_file and not has_flag:
|
||||||
|
state_issues.append(f'{type_name}.{obj_name_val} — есть {kind}.bsl, но нет <xr:PropertyState> для {kind}')
|
||||||
|
elif has_flag and not has_file:
|
||||||
|
state_issues.append(f'{type_name}.{obj_name_val} — есть <xr:PropertyState> для {kind}, но нет {kind}.bsl')
|
||||||
|
|
||||||
|
if state_checked > 0:
|
||||||
|
if not state_issues:
|
||||||
|
r.ok(f'16. Модули заимствованных объектов: пометки расширенных свойств согласованы ({state_checked})')
|
||||||
|
else:
|
||||||
|
for issue in state_issues:
|
||||||
|
r.warn(f'16. {issue}')
|
||||||
|
|
||||||
# --- Breadcrumb: controlled methods (&ИзменениеИКонтроль) drift is not checked here ---
|
# --- Breadcrumb: controlled methods (&ИзменениеИКонтроль) drift is not checked here ---
|
||||||
ctrl_count = 0
|
ctrl_count = 0
|
||||||
for dp, _dn, files in os.walk(config_dir):
|
for dp, _dn, files in os.walk(config_dir):
|
||||||
@@ -31,7 +31,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" <параметры>
|
python ".gemini/skills/db-create/scripts/db-create.py" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -59,14 +59,14 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" <п
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Создать файловую базу
|
# Создать файловую базу
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB"
|
python ".gemini/skills/db-create/scripts/db-create.py" -InfoBasePath "C:\Bases\NewDB"
|
||||||
|
|
||||||
# Создать серверную базу
|
# Создать серверную базу
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test"
|
python ".gemini/skills/db-create/scripts/db-create.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test"
|
||||||
|
|
||||||
# Создать из шаблона CF
|
# Создать из шаблона CF
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf"
|
python ".gemini/skills/db-create/scripts/db-create.py" -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf"
|
||||||
|
|
||||||
# Создать и добавить в список баз
|
# Создать и добавить в список баз
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" -AddToList -ListName "Новая база"
|
python ".gemini/skills/db-create/scripts/db-create.py" -InfoBasePath "C:\Bases\NewDB" -AddToList -ListName "Новая база"
|
||||||
```
|
```
|
||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
# db-create v1.11 — Create 1C information base
|
# db-create v1.14 — Create 1C information base
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
.\db-create.ps1 -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf" -AddToList -ListName "Новая база"
|
.\db-create.ps1 -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf" -AddToList -ListName "Новая база"
|
||||||
#>
|
#>
|
||||||
|
|
||||||
[CmdletBinding()]
|
[CmdletBinding(PositionalBinding=$false)]
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[string]$V8Path,
|
[string]$V8Path,
|
||||||
+31
-17
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-create v1.11 — Create 1C information base
|
# db-create v1.14 — Create 1C information base
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -120,7 +120,6 @@ def assert_extra_args(extra, engine, hints):
|
|||||||
print(
|
print(
|
||||||
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||||
f"({param} cannot extend the ibcmd command)",
|
f"({param} cannot extend the ibcmd command)",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
for k in owned:
|
for k in owned:
|
||||||
@@ -128,7 +127,6 @@ def assert_extra_args(extra, engine, hints):
|
|||||||
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||||
print(
|
print(
|
||||||
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@@ -196,14 +194,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
|||||||
print(
|
print(
|
||||||
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||||
"(use -AdditionalIbcmdArguments)",
|
"(use -AdditionalIbcmdArguments)",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if engine != "ibcmd" and ibcmd_extra:
|
if engine != "ibcmd" and ibcmd_extra:
|
||||||
print(
|
print(
|
||||||
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||||
"(use -AdditionalV8Arguments)",
|
"(use -AdditionalV8Arguments)",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if engine == "ibcmd":
|
if engine == "ibcmd":
|
||||||
@@ -245,14 +241,14 @@ def resolve_v8path(v8path):
|
|||||||
v8path = max(candidates, key=_version_key)
|
v8path = max(candidates, key=_version_key)
|
||||||
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
else:
|
else:
|
||||||
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
print("Error: 1C executable not found. Specify -V8Path")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
if os.path.isdir(v8path):
|
||||||
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
v8path = os.path.join(v8path, exe)
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
@@ -292,7 +288,7 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
|||||||
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
"""
|
"""
|
||||||
if warn_no_user and os.name == "nt" and not has_username:
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
sys.stderr.write(IBCMD_NOUSER_HINT)
|
sys.stdout.write(IBCMD_NOUSER_HINT)
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
r = subprocess.run(cmd, input=b"", capture_output=True)
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
r.stdout = decode_platform_bytes(r.stdout)
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
@@ -313,7 +309,7 @@ def clean_path(value, param=""):
|
|||||||
if len(v) > 3 and v[-1] in "\\/":
|
if len(v) > 3 and v[-1] in "\\/":
|
||||||
v = v[:-1]
|
v = v[:-1]
|
||||||
if '"' in v:
|
if '"' in v:
|
||||||
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
print(f"Error: {param or 'path'} contains a quote character: {value}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v
|
return v
|
||||||
|
|
||||||
@@ -332,11 +328,31 @@ def run_v8(v8path, arguments):
|
|||||||
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
|
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
|
||||||
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
|
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
|
||||||
escape those quotes, so there the command line is handed over ready-made.
|
escape those quotes, so there the command line is handed over ready-made.
|
||||||
|
|
||||||
|
На POSIX аргументы уходят СПИСКОМ, и кавычки, нужные для склейки на Windows, стали бы
|
||||||
|
частью значения: путь с пробелом платформа не находит («Неопределена информационная
|
||||||
|
база»), многословный -comment теряет молча. Поэтому здесь снимается ОДИН слой
|
||||||
|
обрамляющих кавычек. Склеенные ключи (/N"user", /ConfigurationRepositoryF"путь",
|
||||||
|
File="…") не задеты: у них кавычки внутри токена, а не по краям.
|
||||||
"""
|
"""
|
||||||
if os.name == "nt":
|
if os.name == "nt":
|
||||||
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||||
else:
|
else:
|
||||||
cmd = [v8path] + arguments
|
def strip_framing_quotes(a):
|
||||||
|
# Кавычки, которыми мы обрамляем значения ради склейки на Windows, на POSIX
|
||||||
|
# становятся ЧАСТЬЮ значения. Проверено на darwin: путь с пробелом отдельным
|
||||||
|
# токеном даёт «Неопределена информационная база», а склеенный
|
||||||
|
# /ConfigurationRepositoryF"путь с пробелом" — «завершилось с ошибкой»;
|
||||||
|
# без кавычек обе формы работают.
|
||||||
|
if len(a) > 1 and a[0] == '"' and a[-1] == '"':
|
||||||
|
return a[1:-1] # "значение" отдельным токеном
|
||||||
|
if a[0:1] == "/" and a[-1:] == '"' and '"' in a[:-1]:
|
||||||
|
i = a.index('"')
|
||||||
|
return a[:i] + a[i + 1:-1] # /N"имя" -> /Nимя
|
||||||
|
return a # File="…" не трогаем: там кавычки —
|
||||||
|
# часть синтаксиса строки соединения,
|
||||||
|
# и с ними на POSIX всё работает
|
||||||
|
cmd = [v8path] + [strip_framing_quotes(a) for a in arguments]
|
||||||
r = subprocess.run(cmd, input=b"", capture_output=True)
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
r.stdout = decode_platform_bytes(r.stdout)
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
r.stderr = decode_platform_bytes(r.stderr)
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
@@ -401,15 +417,15 @@ def main():
|
|||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if engine == "ibcmd":
|
if engine == "ibcmd":
|
||||||
if not args.InfoBasePath:
|
if not args.InfoBasePath:
|
||||||
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath)")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# --- Validate template ---
|
# --- Validate template ---
|
||||||
if args.UseTemplate and not os.path.exists(args.UseTemplate):
|
if args.UseTemplate and not os.path.exists(args.UseTemplate):
|
||||||
print(f"Error: template file not found: {args.UseTemplate}", file=sys.stderr)
|
print(f"Error: template file not found: {args.UseTemplate}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# --- ibcmd branch (file infobase only) ---
|
# --- ibcmd branch (file infobase only) ---
|
||||||
@@ -436,10 +452,9 @@ def main():
|
|||||||
print(
|
print(
|
||||||
f"Error: exit code 0 but 1Cv8.1CD is missing or empty at {args.InfoBasePath} "
|
f"Error: exit code 0 but 1Cv8.1CD is missing or empty at {args.InfoBasePath} "
|
||||||
"— information base was not created",
|
"— information base was not created",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
print(f"Error creating information base (code: {exit_code})", file=sys.stderr)
|
print(f"Error creating information base (code: {exit_code})")
|
||||||
print_platform_output(result)
|
print_platform_output(result)
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
@@ -496,10 +511,9 @@ def main():
|
|||||||
print(
|
print(
|
||||||
f"Error: exit code 0 but 1Cv8.1CD is missing or empty at {args.InfoBasePath} "
|
f"Error: exit code 0 but 1Cv8.1CD is missing or empty at {args.InfoBasePath} "
|
||||||
"— information base was not created",
|
"— information base was not created",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
print(f"Error creating information base (code: {exit_code})", file=sys.stderr)
|
print(f"Error creating information base (code: {exit_code})")
|
||||||
|
|
||||||
if os.path.isfile(out_file):
|
if os.path.isfile(out_file):
|
||||||
try:
|
try:
|
||||||
@@ -35,7 +35,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" <параметры>
|
python ".gemini/skills/db-dump-cf/scripts/db-dump-cf.py" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -60,11 +60,11 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" <п
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Выгрузка конфигурации (файловая база)
|
# Выгрузка конфигурации (файловая база)
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\config.cf"
|
python ".gemini/skills/db-dump-cf/scripts/db-dump-cf.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\config.cf"
|
||||||
|
|
||||||
# Серверная база
|
# Серверная база
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "config.cf"
|
python ".gemini/skills/db-dump-cf/scripts/db-dump-cf.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "config.cf"
|
||||||
|
|
||||||
# Выгрузка расширения
|
# Выгрузка расширения
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "ext.cfe" -Extension "МоёРасширение"
|
python ".gemini/skills/db-dump-cf/scripts/db-dump-cf.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "ext.cfe" -Extension "МоёРасширение"
|
||||||
```
|
```
|
||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
# db-dump-cf v1.13 — Dump 1C configuration to CF file
|
# db-dump-cf v1.16 — Dump 1C configuration to CF file
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
.\db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "ext.cfe" -Extension "МоёРасширение"
|
.\db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "ext.cfe" -Extension "МоёРасширение"
|
||||||
#>
|
#>
|
||||||
|
|
||||||
[CmdletBinding()]
|
[CmdletBinding(PositionalBinding=$false)]
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[string]$V8Path,
|
[string]$V8Path,
|
||||||
+34
-18
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-dump-cf v1.13 — Dump 1C configuration to CF file
|
# db-dump-cf v1.16 — Dump 1C configuration to CF file
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -120,7 +120,6 @@ def assert_extra_args(extra, engine, hints):
|
|||||||
print(
|
print(
|
||||||
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||||
f"({param} cannot extend the ibcmd command)",
|
f"({param} cannot extend the ibcmd command)",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
for k in owned:
|
for k in owned:
|
||||||
@@ -128,7 +127,6 @@ def assert_extra_args(extra, engine, hints):
|
|||||||
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||||
print(
|
print(
|
||||||
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@@ -196,14 +194,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
|||||||
print(
|
print(
|
||||||
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||||
"(use -AdditionalIbcmdArguments)",
|
"(use -AdditionalIbcmdArguments)",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if engine != "ibcmd" and ibcmd_extra:
|
if engine != "ibcmd" and ibcmd_extra:
|
||||||
print(
|
print(
|
||||||
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||||
"(use -AdditionalV8Arguments)",
|
"(use -AdditionalV8Arguments)",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if engine == "ibcmd":
|
if engine == "ibcmd":
|
||||||
@@ -245,14 +241,14 @@ def resolve_v8path(v8path):
|
|||||||
v8path = max(candidates, key=_version_key)
|
v8path = max(candidates, key=_version_key)
|
||||||
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
else:
|
else:
|
||||||
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
print("Error: 1C executable not found. Specify -V8Path")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
if os.path.isdir(v8path):
|
||||||
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
v8path = os.path.join(v8path, exe)
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
@@ -283,7 +279,7 @@ def assert_infobase_exists(path):
|
|||||||
if not path:
|
if not path:
|
||||||
return
|
return
|
||||||
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
|
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
|
||||||
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
|
print(f"Error: information base not found at {path} (no 1Cv8.1CD)")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
@@ -300,7 +296,7 @@ def clean_path(value, param=""):
|
|||||||
if len(v) > 3 and v[-1] in "\\/":
|
if len(v) > 3 and v[-1] in "\\/":
|
||||||
v = v[:-1]
|
v = v[:-1]
|
||||||
if '"' in v:
|
if '"' in v:
|
||||||
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
print(f"Error: {param or 'path'} contains a quote character: {value}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v
|
return v
|
||||||
|
|
||||||
@@ -319,11 +315,31 @@ def run_v8(v8path, arguments):
|
|||||||
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
|
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
|
||||||
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
|
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
|
||||||
escape those quotes, so there the command line is handed over ready-made.
|
escape those quotes, so there the command line is handed over ready-made.
|
||||||
|
|
||||||
|
На POSIX аргументы уходят СПИСКОМ, и кавычки, нужные для склейки на Windows, стали бы
|
||||||
|
частью значения: путь с пробелом платформа не находит («Неопределена информационная
|
||||||
|
база»), многословный -comment теряет молча. Поэтому здесь снимается ОДИН слой
|
||||||
|
обрамляющих кавычек. Склеенные ключи (/N"user", /ConfigurationRepositoryF"путь",
|
||||||
|
File="…") не задеты: у них кавычки внутри токена, а не по краям.
|
||||||
"""
|
"""
|
||||||
if os.name == "nt":
|
if os.name == "nt":
|
||||||
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||||
else:
|
else:
|
||||||
cmd = [v8path] + arguments
|
def strip_framing_quotes(a):
|
||||||
|
# Кавычки, которыми мы обрамляем значения ради склейки на Windows, на POSIX
|
||||||
|
# становятся ЧАСТЬЮ значения. Проверено на darwin: путь с пробелом отдельным
|
||||||
|
# токеном даёт «Неопределена информационная база», а склеенный
|
||||||
|
# /ConfigurationRepositoryF"путь с пробелом" — «завершилось с ошибкой»;
|
||||||
|
# без кавычек обе формы работают.
|
||||||
|
if len(a) > 1 and a[0] == '"' and a[-1] == '"':
|
||||||
|
return a[1:-1] # "значение" отдельным токеном
|
||||||
|
if a[0:1] == "/" and a[-1:] == '"' and '"' in a[:-1]:
|
||||||
|
i = a.index('"')
|
||||||
|
return a[:i] + a[i + 1:-1] # /N"имя" -> /Nимя
|
||||||
|
return a # File="…" не трогаем: там кавычки —
|
||||||
|
# часть синтаксиса строки соединения,
|
||||||
|
# и с ними на POSIX всё работает
|
||||||
|
cmd = [v8path] + [strip_framing_quotes(a) for a in arguments]
|
||||||
r = subprocess.run(cmd, input=b"", capture_output=True)
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
r.stdout = decode_platform_bytes(r.stdout)
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
r.stderr = decode_platform_bytes(r.stderr)
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
@@ -352,7 +368,7 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
|||||||
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
"""
|
"""
|
||||||
if warn_no_user and os.name == "nt" and not has_username:
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
sys.stderr.write(IBCMD_NOUSER_HINT)
|
sys.stdout.write(IBCMD_NOUSER_HINT)
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
r = subprocess.run(cmd, input=b"", capture_output=True)
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
r.stdout = decode_platform_bytes(r.stdout)
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
@@ -422,10 +438,10 @@ def main():
|
|||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if engine == "ibcmd":
|
if engine == "ibcmd":
|
||||||
if not args.InfoBasePath:
|
if not args.InfoBasePath:
|
||||||
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath)")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# --- Ensure output directory exists ---
|
# --- Ensure output directory exists ---
|
||||||
@@ -436,7 +452,7 @@ def main():
|
|||||||
# --- ibcmd branch (file infobase only) ---
|
# --- ibcmd branch (file infobase only) ---
|
||||||
if engine == "ibcmd":
|
if engine == "ibcmd":
|
||||||
if args.AllExtensions:
|
if args.AllExtensions:
|
||||||
print("Error: ibcmd config save does not support -AllExtensions (use -Extension)", file=sys.stderr)
|
print("Error: ibcmd config save does not support -AllExtensions (use -Extension)")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
arguments = ["infobase", "config", "save", f"--db-path={args.InfoBasePath}"]
|
arguments = ["infobase", "config", "save", f"--db-path={args.InfoBasePath}"]
|
||||||
if args.Extension:
|
if args.Extension:
|
||||||
@@ -459,9 +475,9 @@ def main():
|
|||||||
if exit_code == 0:
|
if exit_code == 0:
|
||||||
print(f"Configuration dumped successfully to: {args.OutputFile}")
|
print(f"Configuration dumped successfully to: {args.OutputFile}")
|
||||||
elif out_missing:
|
elif out_missing:
|
||||||
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — configuration was not dumped", file=sys.stderr)
|
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — configuration was not dumped")
|
||||||
else:
|
else:
|
||||||
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
|
print(f"Error dumping configuration (code: {exit_code})")
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
# --- Temp dir ---
|
# --- Temp dir ---
|
||||||
@@ -509,9 +525,9 @@ def main():
|
|||||||
if exit_code == 0:
|
if exit_code == 0:
|
||||||
print(f"Configuration dumped successfully to: {args.OutputFile}")
|
print(f"Configuration dumped successfully to: {args.OutputFile}")
|
||||||
elif out_missing:
|
elif out_missing:
|
||||||
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — configuration was not dumped", file=sys.stderr)
|
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — configuration was not dumped")
|
||||||
else:
|
else:
|
||||||
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
|
print(f"Error dumping configuration (code: {exit_code})")
|
||||||
|
|
||||||
if os.path.isfile(out_file):
|
if os.path.isfile(out_file):
|
||||||
try:
|
try:
|
||||||
@@ -38,7 +38,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" <параметры>
|
python ".gemini/skills/db-dump-dt/scripts/db-dump-dt.py" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -61,10 +61,10 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" <п
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Выгрузка ИБ (файловая база)
|
# Выгрузка ИБ (файловая база)
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\base.dt"
|
python ".gemini/skills/db-dump-dt/scripts/db-dump-dt.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\base.dt"
|
||||||
|
|
||||||
# Серверная база
|
# Серверная база
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "base.dt"
|
python ".gemini/skills/db-dump-dt/scripts/db-dump-dt.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "base.dt"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Связанные навыки
|
## Связанные навыки
|
||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
# db-dump-dt v1.12 — Dump 1C information base to DT file
|
# db-dump-dt v1.15 — Dump 1C information base to DT file
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
.\db-dump-dt.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "backup.dt"
|
.\db-dump-dt.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "backup.dt"
|
||||||
#>
|
#>
|
||||||
|
|
||||||
[CmdletBinding()]
|
[CmdletBinding(PositionalBinding=$false)]
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[string]$V8Path,
|
[string]$V8Path,
|
||||||
+33
-17
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-dump-dt v1.12 — Dump 1C information base to DT file
|
# db-dump-dt v1.15 — Dump 1C information base to DT file
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -120,7 +120,6 @@ def assert_extra_args(extra, engine, hints):
|
|||||||
print(
|
print(
|
||||||
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||||
f"({param} cannot extend the ibcmd command)",
|
f"({param} cannot extend the ibcmd command)",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
for k in owned:
|
for k in owned:
|
||||||
@@ -128,7 +127,6 @@ def assert_extra_args(extra, engine, hints):
|
|||||||
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||||
print(
|
print(
|
||||||
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@@ -196,14 +194,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
|||||||
print(
|
print(
|
||||||
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||||
"(use -AdditionalIbcmdArguments)",
|
"(use -AdditionalIbcmdArguments)",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if engine != "ibcmd" and ibcmd_extra:
|
if engine != "ibcmd" and ibcmd_extra:
|
||||||
print(
|
print(
|
||||||
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||||
"(use -AdditionalV8Arguments)",
|
"(use -AdditionalV8Arguments)",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if engine == "ibcmd":
|
if engine == "ibcmd":
|
||||||
@@ -245,14 +241,14 @@ def resolve_v8path(v8path):
|
|||||||
v8path = max(candidates, key=_version_key)
|
v8path = max(candidates, key=_version_key)
|
||||||
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
else:
|
else:
|
||||||
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
print("Error: 1C executable not found. Specify -V8Path")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
if os.path.isdir(v8path):
|
||||||
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
v8path = os.path.join(v8path, exe)
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
@@ -283,7 +279,7 @@ def assert_infobase_exists(path):
|
|||||||
if not path:
|
if not path:
|
||||||
return
|
return
|
||||||
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
|
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
|
||||||
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
|
print(f"Error: information base not found at {path} (no 1Cv8.1CD)")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
@@ -300,7 +296,7 @@ def clean_path(value, param=""):
|
|||||||
if len(v) > 3 and v[-1] in "\\/":
|
if len(v) > 3 and v[-1] in "\\/":
|
||||||
v = v[:-1]
|
v = v[:-1]
|
||||||
if '"' in v:
|
if '"' in v:
|
||||||
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
print(f"Error: {param or 'path'} contains a quote character: {value}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v
|
return v
|
||||||
|
|
||||||
@@ -319,11 +315,31 @@ def run_v8(v8path, arguments):
|
|||||||
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
|
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
|
||||||
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
|
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
|
||||||
escape those quotes, so there the command line is handed over ready-made.
|
escape those quotes, so there the command line is handed over ready-made.
|
||||||
|
|
||||||
|
На POSIX аргументы уходят СПИСКОМ, и кавычки, нужные для склейки на Windows, стали бы
|
||||||
|
частью значения: путь с пробелом платформа не находит («Неопределена информационная
|
||||||
|
база»), многословный -comment теряет молча. Поэтому здесь снимается ОДИН слой
|
||||||
|
обрамляющих кавычек. Склеенные ключи (/N"user", /ConfigurationRepositoryF"путь",
|
||||||
|
File="…") не задеты: у них кавычки внутри токена, а не по краям.
|
||||||
"""
|
"""
|
||||||
if os.name == "nt":
|
if os.name == "nt":
|
||||||
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||||
else:
|
else:
|
||||||
cmd = [v8path] + arguments
|
def strip_framing_quotes(a):
|
||||||
|
# Кавычки, которыми мы обрамляем значения ради склейки на Windows, на POSIX
|
||||||
|
# становятся ЧАСТЬЮ значения. Проверено на darwin: путь с пробелом отдельным
|
||||||
|
# токеном даёт «Неопределена информационная база», а склеенный
|
||||||
|
# /ConfigurationRepositoryF"путь с пробелом" — «завершилось с ошибкой»;
|
||||||
|
# без кавычек обе формы работают.
|
||||||
|
if len(a) > 1 and a[0] == '"' and a[-1] == '"':
|
||||||
|
return a[1:-1] # "значение" отдельным токеном
|
||||||
|
if a[0:1] == "/" and a[-1:] == '"' and '"' in a[:-1]:
|
||||||
|
i = a.index('"')
|
||||||
|
return a[:i] + a[i + 1:-1] # /N"имя" -> /Nимя
|
||||||
|
return a # File="…" не трогаем: там кавычки —
|
||||||
|
# часть синтаксиса строки соединения,
|
||||||
|
# и с ними на POSIX всё работает
|
||||||
|
cmd = [v8path] + [strip_framing_quotes(a) for a in arguments]
|
||||||
r = subprocess.run(cmd, input=b"", capture_output=True)
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
r.stdout = decode_platform_bytes(r.stdout)
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
r.stderr = decode_platform_bytes(r.stderr)
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
@@ -352,7 +368,7 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
|||||||
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
"""
|
"""
|
||||||
if warn_no_user and os.name == "nt" and not has_username:
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
sys.stderr.write(IBCMD_NOUSER_HINT)
|
sys.stdout.write(IBCMD_NOUSER_HINT)
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
r = subprocess.run(cmd, input=b"", capture_output=True)
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
r.stdout = decode_platform_bytes(r.stdout)
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
@@ -420,10 +436,10 @@ def main():
|
|||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if engine == "ibcmd":
|
if engine == "ibcmd":
|
||||||
if not args.InfoBasePath:
|
if not args.InfoBasePath:
|
||||||
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath)")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# --- Ensure output directory exists ---
|
# --- Ensure output directory exists ---
|
||||||
@@ -452,9 +468,9 @@ def main():
|
|||||||
if exit_code == 0:
|
if exit_code == 0:
|
||||||
print(f"Information base dumped successfully to: {args.OutputFile}")
|
print(f"Information base dumped successfully to: {args.OutputFile}")
|
||||||
elif out_missing:
|
elif out_missing:
|
||||||
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — information base was not dumped", file=sys.stderr)
|
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — information base was not dumped")
|
||||||
else:
|
else:
|
||||||
print(f"Error dumping information base (code: {exit_code})", file=sys.stderr)
|
print(f"Error dumping information base (code: {exit_code})")
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
# --- Temp dir ---
|
# --- Temp dir ---
|
||||||
@@ -496,9 +512,9 @@ def main():
|
|||||||
if exit_code == 0:
|
if exit_code == 0:
|
||||||
print(f"Information base dumped successfully to: {args.OutputFile}")
|
print(f"Information base dumped successfully to: {args.OutputFile}")
|
||||||
elif out_missing:
|
elif out_missing:
|
||||||
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — information base was not dumped", file=sys.stderr)
|
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — information base was not dumped")
|
||||||
else:
|
else:
|
||||||
print(f"Error dumping information base (code: {exit_code})", file=sys.stderr)
|
print(f"Error dumping information base (code: {exit_code})")
|
||||||
|
|
||||||
if os.path.isfile(out_file):
|
if os.path.isfile(out_file):
|
||||||
try:
|
try:
|
||||||
@@ -33,11 +33,12 @@ allowed-tools:
|
|||||||
Если файла нет — предложи `/db-list add`.
|
Если файла нет — предложи `/db-list add`.
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
Если в записи базы указан `configSrc` — используй как каталог выгрузки по умолчанию.
|
Если в записи базы указан `configSrc` — используй как каталог выгрузки по умолчанию.
|
||||||
|
Для `-Extension` каталог берётся из `extensions[].src` записи базы, если он там указан.
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" <параметры>
|
python ".gemini/skills/db-dump-xml/scripts/db-dump-xml.py" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -76,17 +77,17 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" <
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Полная выгрузка (файловая база)
|
# Полная выгрузка (файловая база)
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full
|
python ".gemini/skills/db-dump-xml/scripts/db-dump-xml.py" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full
|
||||||
|
|
||||||
# Инкрементальная выгрузка
|
# Инкрементальная выгрузка
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Changes
|
python ".gemini/skills/db-dump-xml/scripts/db-dump-xml.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Changes
|
||||||
|
|
||||||
# Частичная выгрузка
|
# Частичная выгрузка
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ"
|
python ".gemini/skills/db-dump-xml/scripts/db-dump-xml.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ"
|
||||||
|
|
||||||
# Серверная база
|
# Серверная база
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -ConfigDir "C:\WS\cfsrc" -Mode Full
|
python ".gemini/skills/db-dump-xml/scripts/db-dump-xml.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -ConfigDir "C:\WS\cfsrc" -Mode Full
|
||||||
|
|
||||||
# Выгрузка расширения
|
# Выгрузка расширения
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
|
python ".gemini/skills/db-dump-xml/scripts/db-dump-xml.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
|
||||||
```
|
```
|
||||||
+135
-7
@@ -1,4 +1,4 @@
|
|||||||
# db-dump-xml v1.15 — Dump 1C configuration to XML files
|
# db-dump-xml v1.21 — Dump 1C configuration to XML files
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
.\db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ"
|
.\db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ"
|
||||||
#>
|
#>
|
||||||
|
|
||||||
[CmdletBinding()]
|
[CmdletBinding(PositionalBinding=$false)]
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[string]$V8Path,
|
[string]$V8Path,
|
||||||
@@ -85,8 +85,10 @@ param(
|
|||||||
[string]$ConfigDir,
|
[string]$ConfigDir,
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[ValidateSet("Full", "Changes", "Partial", "UpdateInfo")]
|
# Пустое значение = режим не задан. Прежнее умолчание Changes подставляется ниже, после
|
||||||
[string]$Mode = "Changes",
|
# того как станет видно, перечислены ли объекты.
|
||||||
|
[ValidateSet("", "Full", "Changes", "Partial", "UpdateInfo")]
|
||||||
|
[string]$Mode = "",
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[string]$Objects,
|
[string]$Objects,
|
||||||
@@ -101,6 +103,18 @@ param(
|
|||||||
[ValidateSet("Hierarchical", "Plain")]
|
[ValidateSet("Hierarchical", "Plain")]
|
||||||
[string]$Format = "Hierarchical",
|
[string]$Format = "Hierarchical",
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string]$ObjectsFile,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string]$RepositoryPath,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string]$RepositoryUser,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string]$RepositoryPassword,
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[string[]]$AdditionalV8Arguments = @(),
|
[string[]]$AdditionalV8Arguments = @(),
|
||||||
|
|
||||||
@@ -111,6 +125,90 @@ param(
|
|||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
# --- Реквизиты хранилища из .v8-project.json ---
|
||||||
|
# Модель их не передаёт: скрипт сопоставляет параметры соединения с записью в databases[]
|
||||||
|
# и берёт repository оттуда. Тот же приём, что в cf-edit.ps1 (сопоставление по configSrc).
|
||||||
|
function Find-V8Project([string]$startDir) {
|
||||||
|
$d = $startDir
|
||||||
|
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||||
|
$pj = Join-Path $d ".v8-project.json"
|
||||||
|
if (Test-Path $pj) { return $pj }
|
||||||
|
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||||
|
if ($parent -eq $d) { break }
|
||||||
|
$d = $parent
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
function Test-SamePath {
|
||||||
|
param([string]$A, [string]$B)
|
||||||
|
if (-not $A -or -not $B) { return $false }
|
||||||
|
try {
|
||||||
|
$na = [System.IO.Path]::GetFullPath($A).TrimEnd('\', '/')
|
||||||
|
$nb = [System.IO.Path]::GetFullPath($B).TrimEnd('\', '/')
|
||||||
|
return $na.Equals($nb, [System.StringComparison]::OrdinalIgnoreCase)
|
||||||
|
} catch { return $false }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Find-ProjectDatabase {
|
||||||
|
# Запись базы в реестре, соответствующая переданному соединению. $null, если не найдена.
|
||||||
|
$pf = Find-V8Project (Get-Location).Path
|
||||||
|
if (-not $pf) { return $null }
|
||||||
|
try { $proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json } catch { return $null }
|
||||||
|
if (-not $proj.databases) { return $null }
|
||||||
|
foreach ($db in $proj.databases) {
|
||||||
|
if ($InfoBasePath -and $db.path -and (Test-SamePath $db.path $InfoBasePath)) { return $db }
|
||||||
|
if ($InfoBaseServer -and $InfoBaseRef -and $db.server -and $db.ref) {
|
||||||
|
if ($db.server.Equals($InfoBaseServer, [System.StringComparison]::OrdinalIgnoreCase) -and
|
||||||
|
$db.ref.Equals($InfoBaseRef, [System.StringComparison]::OrdinalIgnoreCase)) { return $db }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-RepositorySettings {
|
||||||
|
# Возвращает @{ Path; User; Password; FromRegistry }. Явные -Repository* всегда сильнее реестра.
|
||||||
|
$dbRec = Find-ProjectDatabase
|
||||||
|
$rec = $null
|
||||||
|
if ($dbRec) {
|
||||||
|
if ($Extension) {
|
||||||
|
# У расширения СВОЁ хранилище со своим путём (проверено): выбирается парой
|
||||||
|
# /ConfigurationRepositoryF"<путь расширения>" + -Extension "<Имя>".
|
||||||
|
if ($dbRec.extensions) {
|
||||||
|
foreach ($ext in $dbRec.extensions) {
|
||||||
|
if ($ext.name -and $ext.name.Equals($Extension, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||||
|
$rec = $ext.repository
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$rec = $dbRec.repository
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$path = if ($RepositoryPath) { $RepositoryPath } elseif ($rec -and $rec.path) { [string]$rec.path } else { $null }
|
||||||
|
$user = if ($RepositoryUser) { $RepositoryUser } elseif ($rec -and $rec.user) { [string]$rec.user } else { $null }
|
||||||
|
# Пустой пароль = отсутствующий: 1С требует опускать ключ целиком, а не передавать пустое значение.
|
||||||
|
$pwd = if ($RepositoryPassword) { $RepositoryPassword } elseif ($rec -and $rec.password) { [string]$rec.password } else { $null }
|
||||||
|
return @{
|
||||||
|
Path = if ($path) { $path.Trim().Trim('"') } else { $null }
|
||||||
|
User = $user
|
||||||
|
Password = $pwd
|
||||||
|
FromRegistry = [bool]($rec -and $rec.path)
|
||||||
|
DbRecord = $dbRec
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-RepositoryArgs {
|
||||||
|
# Ключи доступа к хранилищу. Форма — кавычки ВНУТРИ токена, как у /N и /P.
|
||||||
|
param([hashtable]$Repo)
|
||||||
|
$a = @()
|
||||||
|
if (-not $Repo -or -not $Repo.Path) { return $a }
|
||||||
|
$a += "/ConfigurationRepositoryF`"$($Repo.Path)`""
|
||||||
|
if ($Repo.User) { $a += "/ConfigurationRepositoryN`"$($Repo.User)`"" }
|
||||||
|
if ($Repo.Password) { $a += "/ConfigurationRepositoryP`"$($Repo.Password)`"" }
|
||||||
|
return $a
|
||||||
|
}
|
||||||
|
|
||||||
function Protect-Secrets {
|
function Protect-Secrets {
|
||||||
# Redact literal secret values from a display string (String.Replace is literal, not regex).
|
# Redact literal secret values from a display string (String.Replace is literal, not regex).
|
||||||
param([string]$Text, [string[]]$Secrets)
|
param([string]$Text, [string[]]$Secrets)
|
||||||
@@ -132,7 +230,7 @@ $script:IbcmdOwnedKeys = @(
|
|||||||
'--import', '--export', '--apply', '--force', '--create-database',
|
'--import', '--export', '--apply', '--force', '--create-database',
|
||||||
'--user', '--password'
|
'--user', '--password'
|
||||||
)
|
)
|
||||||
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
|
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP', '/ConfigurationRepositoryP')
|
||||||
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
|
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
|
||||||
|
|
||||||
function Test-ArgKeyMatch {
|
function Test-ArgKeyMatch {
|
||||||
@@ -415,8 +513,33 @@ if ($engine -eq "ibcmd") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- Validate Partial mode ---
|
# --- Validate Partial mode ---
|
||||||
|
# Список объектов приходит либо строкой, либо файлом: файл нужен, чтобы не перепечатывать
|
||||||
|
# то, что уже напечатал другой навык (например /db-repo update со списком полученных объектов).
|
||||||
|
if ($ObjectsFile) {
|
||||||
|
if (-not (Test-Path $ObjectsFile)) {
|
||||||
|
Write-Host "Error: -ObjectsFile not found: $ObjectsFile" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
$fromFile = @([System.IO.File]::ReadAllLines($ObjectsFile, [System.Text.Encoding]::UTF8) |
|
||||||
|
ForEach-Object { $_.Trim() } | Where-Object { $_ -and -not $_.StartsWith('#') })
|
||||||
|
$Objects = (@(@($Objects -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) + $fromFile) -join ',')
|
||||||
|
}
|
||||||
|
# Перечислены объекты — операция частичная. Иначе список молча игнорировался бы: умолчание
|
||||||
|
# Changes выгружает «изменённое с прошлой выгрузки», а не то, что просили.
|
||||||
|
if ($Objects) {
|
||||||
|
if ($Mode -eq "UpdateInfo") {
|
||||||
|
# Не «шире/уже», а другая операция: обновление ConfigDumpInfo без выгрузки файлов.
|
||||||
|
Write-Host "Error: -Mode UpdateInfo does not take an object list — it only refreshes ConfigDumpInfo.xml" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Mode -eq "Full" -or $Mode -eq "Changes") {
|
||||||
|
Write-Host "[note] перечислены объекты — выгружаются только они; -Mode $Mode не применён" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
$Mode = "Partial"
|
||||||
|
}
|
||||||
|
if (-not $Mode) { $Mode = "Changes" }
|
||||||
if ($Mode -eq "Partial" -and -not $Objects) {
|
if ($Mode -eq "Partial" -and -not $Objects) {
|
||||||
Write-Host "Error: -Objects required for Partial mode" -ForegroundColor Red
|
Write-Host "Error: -Objects or -ObjectsFile required for Partial mode" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -486,6 +609,11 @@ try {
|
|||||||
if ($UserName) { $arguments += "/N`"$UserName`"" }
|
if ($UserName) { $arguments += "/N`"$UserName`"" }
|
||||||
if ($Password) { $arguments += "/P`"$Password`"" }
|
if ($Password) { $arguments += "/P`"$Password`"" }
|
||||||
|
|
||||||
|
# База под хранилищем не примет НИ ОДНОЙ операции конфигуратора без этих реквизитов, а для
|
||||||
|
# базы вне хранилища они безвредны — поэтому подставляем всегда, когда они известны.
|
||||||
|
$__repo = Resolve-RepositorySettings
|
||||||
|
$arguments += Get-RepositoryArgs $__repo
|
||||||
|
|
||||||
$arguments += "/DumpConfigToFiles", "`"$ConfigDir`""
|
$arguments += "/DumpConfigToFiles", "`"$ConfigDir`""
|
||||||
$arguments += "-Format", $Format
|
$arguments += "-Format", $Format
|
||||||
|
|
||||||
@@ -530,7 +658,7 @@ try {
|
|||||||
$arguments += $extraArgs
|
$arguments += $extraArgs
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName, $__repo.Password))"
|
||||||
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
|
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
|
||||||
$exitCode = $__v8.ExitCode
|
$exitCode = $__v8.ExitCode
|
||||||
|
|
||||||
+159
-24
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-dump-xml v1.15 — Dump 1C configuration to XML files
|
# db-dump-xml v1.21 — Dump 1C configuration to XML files
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -72,10 +72,95 @@ IBCMD_OWNED_KEYS = [
|
|||||||
"--import", "--export", "--apply", "--force", "--create-database",
|
"--import", "--export", "--apply", "--force", "--create-database",
|
||||||
"--user", "--password",
|
"--user", "--password",
|
||||||
]
|
]
|
||||||
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
|
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP", "/ConfigurationRepositoryP"]
|
||||||
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
|
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
|
||||||
|
|
||||||
|
|
||||||
|
# --- Реквизиты хранилища из .v8-project.json ---
|
||||||
|
# Модель их не передаёт: скрипт сопоставляет параметры соединения с записью в databases[]
|
||||||
|
# и берёт repository оттуда. Тот же приём, что в cf-edit.py (сопоставление по configSrc).
|
||||||
|
def _sg_find_v8project(start_dir):
|
||||||
|
d = start_dir
|
||||||
|
for _ in range(20):
|
||||||
|
if not d:
|
||||||
|
break
|
||||||
|
pj = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pj):
|
||||||
|
return pj
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
break
|
||||||
|
d = parent
|
||||||
|
return None
|
||||||
|
|
||||||
|
def same_path(a, b):
|
||||||
|
if not a or not b:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
return os.path.abspath(a).rstrip("\\/").lower() == os.path.abspath(b).rstrip("\\/").lower()
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def find_project_database(args):
|
||||||
|
"""Запись базы в реестре, соответствующая переданному соединению. None, если не найдена."""
|
||||||
|
pf = _sg_find_v8project(os.getcwd())
|
||||||
|
if not pf:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
proj = json.load(f)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
for db in proj.get("databases") or []:
|
||||||
|
if args.InfoBasePath and db.get("path") and same_path(db["path"], args.InfoBasePath):
|
||||||
|
return db
|
||||||
|
if args.InfoBaseServer and args.InfoBaseRef and db.get("server") and db.get("ref"):
|
||||||
|
if (db["server"].lower() == args.InfoBaseServer.lower()
|
||||||
|
and db["ref"].lower() == args.InfoBaseRef.lower()):
|
||||||
|
return db
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_repository_settings(args):
|
||||||
|
"""Возвращает dict path/user/password/from_registry. Явные -Repository* сильнее реестра."""
|
||||||
|
db_rec = find_project_database(args)
|
||||||
|
rec = None
|
||||||
|
if db_rec:
|
||||||
|
if args.Extension:
|
||||||
|
# У расширения СВОЁ хранилище со своим путём (проверено): выбирается парой
|
||||||
|
# /ConfigurationRepositoryF"<путь расширения>" + -Extension "<Имя>".
|
||||||
|
for ext in db_rec.get("extensions") or []:
|
||||||
|
if (ext.get("name") or "").lower() == args.Extension.lower():
|
||||||
|
rec = ext.get("repository")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
rec = db_rec.get("repository")
|
||||||
|
path = args.RepositoryPath or ((rec or {}).get("path") or None)
|
||||||
|
user = args.RepositoryUser or ((rec or {}).get("user") or None)
|
||||||
|
# Пустой пароль = отсутствующий: 1С требует опускать ключ целиком, а не передавать пустое значение.
|
||||||
|
pwd = args.RepositoryPassword or ((rec or {}).get("password") or None)
|
||||||
|
return {
|
||||||
|
"path": path.strip().strip('"') if path else None,
|
||||||
|
"user": user,
|
||||||
|
"password": pwd,
|
||||||
|
"from_registry": bool(rec and rec.get("path")),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def repository_args(repo):
|
||||||
|
"""Ключи доступа к хранилищу. Форма — кавычки ВНУТРИ токена, как у /N и /P."""
|
||||||
|
a = []
|
||||||
|
if not repo or not repo.get("path"):
|
||||||
|
return a
|
||||||
|
a.append('/ConfigurationRepositoryF"%s"' % repo["path"])
|
||||||
|
if repo.get("user"):
|
||||||
|
a.append('/ConfigurationRepositoryN"%s"' % repo["user"])
|
||||||
|
if repo.get("password"):
|
||||||
|
a.append('/ConfigurationRepositoryP"%s"' % repo["password"])
|
||||||
|
return a
|
||||||
|
|
||||||
|
|
||||||
def arg_key_match(token, key):
|
def arg_key_match(token, key):
|
||||||
"""Token matches a key when it equals it, or starts with it and the next character
|
"""Token matches a key when it equals it, or starts with it and the next character
|
||||||
is not a letter — catches glued /N"user" and --password=x, while keeping
|
is not a letter — catches glued /N"user" and --password=x, while keeping
|
||||||
@@ -120,7 +205,6 @@ def assert_extra_args(extra, engine, hints):
|
|||||||
print(
|
print(
|
||||||
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||||
f"({param} cannot extend the ibcmd command)",
|
f"({param} cannot extend the ibcmd command)",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
for k in owned:
|
for k in owned:
|
||||||
@@ -128,7 +212,6 @@ def assert_extra_args(extra, engine, hints):
|
|||||||
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||||
print(
|
print(
|
||||||
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@@ -196,14 +279,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
|||||||
print(
|
print(
|
||||||
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||||
"(use -AdditionalIbcmdArguments)",
|
"(use -AdditionalIbcmdArguments)",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if engine != "ibcmd" and ibcmd_extra:
|
if engine != "ibcmd" and ibcmd_extra:
|
||||||
print(
|
print(
|
||||||
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||||
"(use -AdditionalV8Arguments)",
|
"(use -AdditionalV8Arguments)",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if engine == "ibcmd":
|
if engine == "ibcmd":
|
||||||
@@ -245,14 +326,14 @@ def resolve_v8path(v8path):
|
|||||||
v8path = max(candidates, key=_version_key)
|
v8path = max(candidates, key=_version_key)
|
||||||
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
else:
|
else:
|
||||||
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
print("Error: 1C executable not found. Specify -V8Path")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
if os.path.isdir(v8path):
|
||||||
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
v8path = os.path.join(v8path, exe)
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
@@ -283,7 +364,7 @@ def assert_infobase_exists(path):
|
|||||||
if not path:
|
if not path:
|
||||||
return
|
return
|
||||||
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
|
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
|
||||||
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
|
print(f"Error: information base not found at {path} (no 1Cv8.1CD)")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
@@ -300,7 +381,7 @@ def clean_path(value, param=""):
|
|||||||
if len(v) > 3 and v[-1] in "\\/":
|
if len(v) > 3 and v[-1] in "\\/":
|
||||||
v = v[:-1]
|
v = v[:-1]
|
||||||
if '"' in v:
|
if '"' in v:
|
||||||
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
print(f"Error: {param or 'path'} contains a quote character: {value}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v
|
return v
|
||||||
|
|
||||||
@@ -319,11 +400,31 @@ def run_v8(v8path, arguments):
|
|||||||
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
|
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
|
||||||
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
|
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
|
||||||
escape those quotes, so there the command line is handed over ready-made.
|
escape those quotes, so there the command line is handed over ready-made.
|
||||||
|
|
||||||
|
На POSIX аргументы уходят СПИСКОМ, и кавычки, нужные для склейки на Windows, стали бы
|
||||||
|
частью значения: путь с пробелом платформа не находит («Неопределена информационная
|
||||||
|
база»), многословный -comment теряет молча. Поэтому здесь снимается ОДИН слой
|
||||||
|
обрамляющих кавычек. Склеенные ключи (/N"user", /ConfigurationRepositoryF"путь",
|
||||||
|
File="…") не задеты: у них кавычки внутри токена, а не по краям.
|
||||||
"""
|
"""
|
||||||
if os.name == "nt":
|
if os.name == "nt":
|
||||||
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||||
else:
|
else:
|
||||||
cmd = [v8path] + arguments
|
def strip_framing_quotes(a):
|
||||||
|
# Кавычки, которыми мы обрамляем значения ради склейки на Windows, на POSIX
|
||||||
|
# становятся ЧАСТЬЮ значения. Проверено на darwin: путь с пробелом отдельным
|
||||||
|
# токеном даёт «Неопределена информационная база», а склеенный
|
||||||
|
# /ConfigurationRepositoryF"путь с пробелом" — «завершилось с ошибкой»;
|
||||||
|
# без кавычек обе формы работают.
|
||||||
|
if len(a) > 1 and a[0] == '"' and a[-1] == '"':
|
||||||
|
return a[1:-1] # "значение" отдельным токеном
|
||||||
|
if a[0:1] == "/" and a[-1:] == '"' and '"' in a[:-1]:
|
||||||
|
i = a.index('"')
|
||||||
|
return a[:i] + a[i + 1:-1] # /N"имя" -> /Nимя
|
||||||
|
return a # File="…" не трогаем: там кавычки —
|
||||||
|
# часть синтаксиса строки соединения,
|
||||||
|
# и с ними на POSIX всё работает
|
||||||
|
cmd = [v8path] + [strip_framing_quotes(a) for a in arguments]
|
||||||
r = subprocess.run(cmd, input=b"", capture_output=True)
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
r.stdout = decode_platform_bytes(r.stdout)
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
r.stderr = decode_platform_bytes(r.stderr)
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
@@ -352,7 +453,7 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
|||||||
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
"""
|
"""
|
||||||
if warn_no_user and os.name == "nt" and not has_username:
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
sys.stderr.write(IBCMD_NOUSER_HINT)
|
sys.stdout.write(IBCMD_NOUSER_HINT)
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
r = subprocess.run(cmd, input=b"", capture_output=True)
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
r.stdout = decode_platform_bytes(r.stdout)
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
@@ -388,14 +489,18 @@ def main():
|
|||||||
parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server")
|
parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server")
|
||||||
parser.add_argument("-UserName", default="", help="1C user name")
|
parser.add_argument("-UserName", default="", help="1C user name")
|
||||||
parser.add_argument("-Password", default="", help="1C user password")
|
parser.add_argument("-Password", default="", help="1C user password")
|
||||||
|
parser.add_argument("-RepositoryPath", default="")
|
||||||
|
parser.add_argument("-RepositoryUser", default="")
|
||||||
|
parser.add_argument("-RepositoryPassword", default="")
|
||||||
parser.add_argument("-ConfigDir", required=True, help="Directory for configuration dump")
|
parser.add_argument("-ConfigDir", required=True, help="Directory for configuration dump")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-Mode",
|
"-Mode",
|
||||||
default="Changes",
|
default="",
|
||||||
choices=["Full", "Changes", "Partial", "UpdateInfo"],
|
choices=["", "Full", "Changes", "Partial", "UpdateInfo"],
|
||||||
help="Dump mode (default: Changes)",
|
help="Dump mode (default: Changes)",
|
||||||
)
|
)
|
||||||
parser.add_argument("-Objects", default="", help="Comma-separated metadata object names (for Partial mode)")
|
parser.add_argument("-Objects", default="", help="Comma-separated metadata object names (for Partial mode)")
|
||||||
|
parser.add_argument("-ObjectsFile", default="")
|
||||||
parser.add_argument("-Extension", default="", help="Extension name to dump")
|
parser.add_argument("-Extension", default="", help="Extension name to dump")
|
||||||
parser.add_argument("-AllExtensions", action="store_true", help="Dump all extensions")
|
parser.add_argument("-AllExtensions", action="store_true", help="Dump all extensions")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
@@ -436,15 +541,40 @@ def main():
|
|||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if engine == "ibcmd":
|
if engine == "ibcmd":
|
||||||
if not args.InfoBasePath:
|
if not args.InfoBasePath:
|
||||||
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath)")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# --- Validate Partial mode ---
|
# --- Validate Partial mode ---
|
||||||
|
# Список объектов приходит либо строкой, либо файлом: файл нужен, чтобы не перепечатывать
|
||||||
|
# то, что уже напечатал другой навык (например /db-repo update со списком полученных объектов).
|
||||||
|
if args.ObjectsFile:
|
||||||
|
if not os.path.exists(args.ObjectsFile):
|
||||||
|
print("Error: -ObjectsFile not found: %s" % args.ObjectsFile)
|
||||||
|
sys.exit(1)
|
||||||
|
with open(args.ObjectsFile, encoding="utf-8-sig") as f:
|
||||||
|
from_file = [s.strip() for s in f.read().splitlines()
|
||||||
|
if s.strip() and not s.strip().startswith("#")]
|
||||||
|
inline = [s.strip() for s in args.Objects.split(",") if s.strip()]
|
||||||
|
args.Objects = ",".join(inline + from_file)
|
||||||
|
# Перечислены объекты — операция частичная. Иначе список молча игнорировался бы: умолчание
|
||||||
|
# Changes выгружает «изменённое с прошлой выгрузки», а не то, что просили.
|
||||||
|
if args.Objects:
|
||||||
|
if args.Mode == "UpdateInfo":
|
||||||
|
# Не «шире/уже», а другая операция: обновление ConfigDumpInfo без выгрузки файлов.
|
||||||
|
print("Error: -Mode UpdateInfo does not take an object list — it only refreshes "
|
||||||
|
"ConfigDumpInfo.xml")
|
||||||
|
sys.exit(1)
|
||||||
|
if args.Mode in ("Full", "Changes"):
|
||||||
|
print("[note] перечислены объекты — выгружаются только они; -Mode %s не применён"
|
||||||
|
% args.Mode)
|
||||||
|
args.Mode = "Partial"
|
||||||
|
if not args.Mode:
|
||||||
|
args.Mode = "Changes"
|
||||||
if args.Mode == "Partial" and not args.Objects:
|
if args.Mode == "Partial" and not args.Objects:
|
||||||
print("Error: -Objects required for Partial mode", file=sys.stderr)
|
print("Error: -Objects or -ObjectsFile required for Partial mode")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# --- Create output dir if needed ---
|
# --- Create output dir if needed ---
|
||||||
@@ -455,12 +585,12 @@ def main():
|
|||||||
# --- ibcmd branch (file infobase only; hierarchical Full/Changes) ---
|
# --- ibcmd branch (file infobase only; hierarchical Full/Changes) ---
|
||||||
if engine == "ibcmd":
|
if engine == "ibcmd":
|
||||||
if args.Format == "Plain":
|
if args.Format == "Plain":
|
||||||
print("Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)", file=sys.stderr)
|
print("Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if args.AllExtensions:
|
if args.AllExtensions:
|
||||||
arguments = ["infobase", "config", "export", "all-extensions", args.ConfigDir, f"--db-path={args.InfoBasePath}"]
|
arguments = ["infobase", "config", "export", "all-extensions", args.ConfigDir, f"--db-path={args.InfoBasePath}"]
|
||||||
elif args.Mode == "UpdateInfo":
|
elif args.Mode == "UpdateInfo":
|
||||||
print("Error: ibcmd config export does not support Mode UpdateInfo; use 1cv8", file=sys.stderr)
|
print("Error: ibcmd config export does not support Mode UpdateInfo; use 1cv8")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
elif args.Mode == "Partial":
|
elif args.Mode == "Partial":
|
||||||
obj_list = [o.strip() for o in args.Objects.split(",") if o.strip()]
|
obj_list = [o.strip() for o in args.Objects.split(",") if o.strip()]
|
||||||
@@ -490,9 +620,9 @@ def main():
|
|||||||
if exit_code == 0:
|
if exit_code == 0:
|
||||||
print(f"Configuration exported successfully to: {args.ConfigDir}")
|
print(f"Configuration exported successfully to: {args.ConfigDir}")
|
||||||
elif out_missing:
|
elif out_missing:
|
||||||
print(f"Error: exit code 0 but no files under {args.ConfigDir} — configuration was not exported", file=sys.stderr)
|
print(f"Error: exit code 0 but no files under {args.ConfigDir} — configuration was not exported")
|
||||||
else:
|
else:
|
||||||
print(f"Error exporting configuration (code: {exit_code})", file=sys.stderr)
|
print(f"Error exporting configuration (code: {exit_code})")
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
# --- Temp dir ---
|
# --- Temp dir ---
|
||||||
@@ -513,6 +643,11 @@ def main():
|
|||||||
if args.Password:
|
if args.Password:
|
||||||
arguments.append(f'/P"{args.Password}"')
|
arguments.append(f'/P"{args.Password}"')
|
||||||
|
|
||||||
|
# База под хранилищем не примет НИ ОДНОЙ операции конфигуратора без этих реквизитов, а для
|
||||||
|
# базы вне хранилища они безвредны — поэтому подставляем всегда, когда они известны.
|
||||||
|
repo = resolve_repository_settings(args)
|
||||||
|
arguments.extend(repository_args(repo))
|
||||||
|
|
||||||
arguments += ["/DumpConfigToFiles", f'"{args.ConfigDir}"']
|
arguments += ["/DumpConfigToFiles", f'"{args.ConfigDir}"']
|
||||||
arguments += ["-Format", args.Format]
|
arguments += ["-Format", args.Format]
|
||||||
|
|
||||||
@@ -551,7 +686,7 @@ def main():
|
|||||||
arguments.extend(quote_if_needed(a) for a in extra_args)
|
arguments.extend(quote_if_needed(a) for a in extra_args)
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName, repo['password'])}")
|
||||||
result = run_v8(v8path, arguments)
|
result = run_v8(v8path, arguments)
|
||||||
exit_code = result.returncode
|
exit_code = result.returncode
|
||||||
|
|
||||||
@@ -564,9 +699,9 @@ def main():
|
|||||||
print("Dump completed successfully")
|
print("Dump completed successfully")
|
||||||
print(f"Configuration dumped to: {args.ConfigDir}")
|
print(f"Configuration dumped to: {args.ConfigDir}")
|
||||||
elif out_missing:
|
elif out_missing:
|
||||||
print(f"Error: exit code 0 but no files under {args.ConfigDir} — configuration was not dumped", file=sys.stderr)
|
print(f"Error: exit code 0 but no files under {args.ConfigDir} — configuration was not dumped")
|
||||||
else:
|
else:
|
||||||
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
|
print(f"Error dumping configuration (code: {exit_code})")
|
||||||
|
|
||||||
if os.path.isfile(out_file):
|
if os.path.isfile(out_file):
|
||||||
try:
|
try:
|
||||||
@@ -40,7 +40,19 @@ allowed-tools:
|
|||||||
"password": "",
|
"password": "",
|
||||||
"aliases": ["dev", "разработка"],
|
"aliases": ["dev", "разработка"],
|
||||||
"branches": ["dev", "develop", "feature/*"],
|
"branches": ["dev", "develop", "feature/*"],
|
||||||
"configSrc": "C:\\WS\\myapp\\cfsrc"
|
"configSrc": "C:\\WS\\myapp\\cfsrc",
|
||||||
|
"repository": {
|
||||||
|
"path": "\\\\srv01\\repo\\MyApp",
|
||||||
|
"user": "Ivanov",
|
||||||
|
"password": ""
|
||||||
|
},
|
||||||
|
"extensions": [
|
||||||
|
{
|
||||||
|
"name": "МоёРасширение",
|
||||||
|
"src": "src\\cfe\\МоёРасширение",
|
||||||
|
"repository": { "path": "\\\\srv01\\repo\\MyApp_Ext", "user": "Ivanov", "password": "" }
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "test",
|
"id": "test",
|
||||||
@@ -64,6 +76,7 @@ allowed-tools:
|
|||||||
| `v8path` | string | Каталог bin платформы 1С. Необязательный — если не задан, автоопределение |
|
| `v8path` | string | Каталог bin платформы 1С. Необязательный — если не задан, автоопределение |
|
||||||
| `v8args` | array | Доп. аргументы запуска `1cv8.exe` для всех навыков, напр. `["/UseHwLicenses+"]` |
|
| `v8args` | array | Доп. аргументы запуска `1cv8.exe` для всех навыков, напр. `["/UseHwLicenses+"]` |
|
||||||
| `ibcmdargs` | array | Доп. аргументы `ibcmd` (форма `--ключ=значение`) |
|
| `ibcmdargs` | array | Доп. аргументы `ibcmd` (форма `--ключ=значение`) |
|
||||||
|
| `newObjectPosition` | `"end"` / `"byName"` | Куда попадает новый объект в составе конфигурации: в конец своего вида (по умолчанию) или на место по имени. Переопределяется в `databases[]` |
|
||||||
| `databases` | array | Массив баз данных |
|
| `databases` | array | Массив баз данных |
|
||||||
| `default` | string | id базы по умолчанию |
|
| `default` | string | id базы по умолчанию |
|
||||||
|
|
||||||
@@ -82,6 +95,35 @@ allowed-tools:
|
|||||||
| `aliases` | string[] | нет | Альтернативные имена для быстрого доступа |
|
| `aliases` | string[] | нет | Альтернативные имена для быстрого доступа |
|
||||||
| `branches` | string[] | нет | Git-ветки или glob-паттерны (`release/*`, `feature/*`), привязанные к этой базе |
|
| `branches` | string[] | нет | Git-ветки или glob-паттерны (`release/*`, `feature/*`), привязанные к этой базе |
|
||||||
| `configSrc` | string | нет | Каталог XML-выгрузки конфигурации |
|
| `configSrc` | string | нет | Каталог XML-выгрузки конфигурации |
|
||||||
|
| `repository` | object | нет | Хранилище конфигурации: `path`, `user`, `password` (см. ниже) |
|
||||||
|
| `extensions` | array | нет | Расширения: `name`, `src`, необязательное `repository` (см. ниже) |
|
||||||
|
|
||||||
|
### Хранилище конфигурации
|
||||||
|
|
||||||
|
База, подключённая к хранилищу конфигурации 1С, **не принимает ни одной операции конфигуратора**
|
||||||
|
без реквизитов доступа к хранилищу — это касается не только `/db-repo`, но и `/db-load-xml`,
|
||||||
|
`/db-dump-xml`, `/db-update`, `/db-load-git`. Реквизиты берутся из `repository` записи базы,
|
||||||
|
передавать их в каждом вызове не нужно.
|
||||||
|
|
||||||
|
| Поле | Тип | Обязательное | Описание |
|
||||||
|
|------|-----|:------------:|----------|
|
||||||
|
| `repository.path` | string | да | Каталог хранилища или `tcp://<хост>[:<порт>]/<имя>` |
|
||||||
|
| `repository.user` | string | нет | Пользователь **хранилища**. Не наследуется от `user` базы |
|
||||||
|
| `repository.password` | string | нет | Пароль пользователя хранилища |
|
||||||
|
|
||||||
|
У расширения **своё хранилище** со своим путём, поэтому одного `repository` мало:
|
||||||
|
|
||||||
|
| Поле | Тип | Обязательное | Описание |
|
||||||
|
|------|-----|:------------:|----------|
|
||||||
|
| `extensions[].name` | string | да | Имя расширения, как в конфигурации |
|
||||||
|
| `extensions[].src` | string | нет | Каталог XML-исходников расширения |
|
||||||
|
| `extensions[].repository` | object | нет | Хранилище расширения. Расширение без хранилища — обычный случай |
|
||||||
|
|
||||||
|
Пароль хранилища — такой же секрет, как `password` базы; `.v8-project.json` в `.gitignore`.
|
||||||
|
|
||||||
|
> **Сетевое хранилище.** Адрес — `tcp://<хост>[:<порт>]/<имя>`, порт по умолчанию 1542.
|
||||||
|
> Обслуживается сервером хранилища. Если он недоступен, платформа отвечает «Соединение с
|
||||||
|
> хранилищем конфигурации не установлено» — тем же сообщением, что и при отсутствии реквизитов.
|
||||||
|
|
||||||
## Алгоритм разрешения базы данных
|
## Алгоритм разрешения базы данных
|
||||||
|
|
||||||
@@ -128,6 +170,7 @@ test Тестовая server srv01/MyApp_Test
|
|||||||
- path (для file) или server + ref (для server)
|
- path (для file) или server + ref (для server)
|
||||||
- user, password (необязательно)
|
- user, password (необязательно)
|
||||||
- aliases, branches (необязательно)
|
- aliases, branches (необязательно)
|
||||||
|
- если база под хранилищем конфигурации — `repository`: путь, пользователь, пароль
|
||||||
|
|
||||||
Добавь в массив `databases`. Если это первая база — установи как `default`.
|
Добавь в массив `databases`. Если это первая база — установи как `default`.
|
||||||
|
|
||||||
@@ -159,3 +202,10 @@ test Тестовая server srv01/MyApp_Test
|
|||||||
```
|
```
|
||||||
|
|
||||||
> **Важно**: между `/N` и именем пробела нет. Между `/P` и паролем пробела нет. Если пароль пустой — опусти `/P` целиком.
|
> **Важно**: между `/N` и именем пробела нет. Между `/P` и паролем пробела нет. Если пароль пустой — опусти `/P` целиком.
|
||||||
|
|
||||||
|
**Хранилище конфигурации** (если у базы задан `repository`) — скрипты навыков подставляют
|
||||||
|
сами, сопоставляя параметры соединения с записью реестра:
|
||||||
|
```
|
||||||
|
/ConfigurationRepositoryF"<path>" /ConfigurationRepositoryN"<user>" /ConfigurationRepositoryP"<password>"
|
||||||
|
```
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" <параметры>
|
python ".gemini/skills/db-load-cf/scripts/db-load-cf.py" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -65,11 +65,11 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" <п
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Файловая база
|
# Файловая база
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\config.cf"
|
python ".gemini/skills/db-load-cf/scripts/db-load-cf.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\config.cf"
|
||||||
|
|
||||||
# Серверная база
|
# Серверная база
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "config.cf"
|
python ".gemini/skills/db-load-cf/scripts/db-load-cf.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "config.cf"
|
||||||
|
|
||||||
# Загрузка расширения
|
# Загрузка расширения
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "ext.cfe" -Extension "МоёРасширение"
|
python ".gemini/skills/db-load-cf/scripts/db-load-cf.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "ext.cfe" -Extension "МоёРасширение"
|
||||||
```
|
```
|
||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
# db-load-cf v1.14 — Load 1C configuration from CF file
|
# db-load-cf v1.17 — Load 1C configuration from CF file
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
.\db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "ext.cfe" -Extension "МоёРасширение"
|
.\db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "ext.cfe" -Extension "МоёРасширение"
|
||||||
#>
|
#>
|
||||||
|
|
||||||
[CmdletBinding()]
|
[CmdletBinding(PositionalBinding=$false)]
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[string]$V8Path,
|
[string]$V8Path,
|
||||||
+33
-17
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-load-cf v1.14 — Load 1C configuration from CF file
|
# db-load-cf v1.17 — Load 1C configuration from CF file
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -120,7 +120,6 @@ def assert_extra_args(extra, engine, hints):
|
|||||||
print(
|
print(
|
||||||
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||||
f"({param} cannot extend the ibcmd command)",
|
f"({param} cannot extend the ibcmd command)",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
for k in owned:
|
for k in owned:
|
||||||
@@ -128,7 +127,6 @@ def assert_extra_args(extra, engine, hints):
|
|||||||
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||||
print(
|
print(
|
||||||
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@@ -196,14 +194,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
|||||||
print(
|
print(
|
||||||
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||||
"(use -AdditionalIbcmdArguments)",
|
"(use -AdditionalIbcmdArguments)",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if engine != "ibcmd" and ibcmd_extra:
|
if engine != "ibcmd" and ibcmd_extra:
|
||||||
print(
|
print(
|
||||||
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||||
"(use -AdditionalV8Arguments)",
|
"(use -AdditionalV8Arguments)",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if engine == "ibcmd":
|
if engine == "ibcmd":
|
||||||
@@ -245,14 +241,14 @@ def resolve_v8path(v8path):
|
|||||||
v8path = max(candidates, key=_version_key)
|
v8path = max(candidates, key=_version_key)
|
||||||
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
else:
|
else:
|
||||||
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
print("Error: 1C executable not found. Specify -V8Path")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
if os.path.isdir(v8path):
|
||||||
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
v8path = os.path.join(v8path, exe)
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
@@ -283,7 +279,7 @@ def assert_infobase_exists(path):
|
|||||||
if not path:
|
if not path:
|
||||||
return
|
return
|
||||||
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
|
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
|
||||||
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
|
print(f"Error: information base not found at {path} (no 1Cv8.1CD)")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
@@ -300,7 +296,7 @@ def clean_path(value, param=""):
|
|||||||
if len(v) > 3 and v[-1] in "\\/":
|
if len(v) > 3 and v[-1] in "\\/":
|
||||||
v = v[:-1]
|
v = v[:-1]
|
||||||
if '"' in v:
|
if '"' in v:
|
||||||
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
print(f"Error: {param or 'path'} contains a quote character: {value}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v
|
return v
|
||||||
|
|
||||||
@@ -319,11 +315,31 @@ def run_v8(v8path, arguments):
|
|||||||
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
|
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
|
||||||
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
|
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
|
||||||
escape those quotes, so there the command line is handed over ready-made.
|
escape those quotes, so there the command line is handed over ready-made.
|
||||||
|
|
||||||
|
На POSIX аргументы уходят СПИСКОМ, и кавычки, нужные для склейки на Windows, стали бы
|
||||||
|
частью значения: путь с пробелом платформа не находит («Неопределена информационная
|
||||||
|
база»), многословный -comment теряет молча. Поэтому здесь снимается ОДИН слой
|
||||||
|
обрамляющих кавычек. Склеенные ключи (/N"user", /ConfigurationRepositoryF"путь",
|
||||||
|
File="…") не задеты: у них кавычки внутри токена, а не по краям.
|
||||||
"""
|
"""
|
||||||
if os.name == "nt":
|
if os.name == "nt":
|
||||||
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||||
else:
|
else:
|
||||||
cmd = [v8path] + arguments
|
def strip_framing_quotes(a):
|
||||||
|
# Кавычки, которыми мы обрамляем значения ради склейки на Windows, на POSIX
|
||||||
|
# становятся ЧАСТЬЮ значения. Проверено на darwin: путь с пробелом отдельным
|
||||||
|
# токеном даёт «Неопределена информационная база», а склеенный
|
||||||
|
# /ConfigurationRepositoryF"путь с пробелом" — «завершилось с ошибкой»;
|
||||||
|
# без кавычек обе формы работают.
|
||||||
|
if len(a) > 1 and a[0] == '"' and a[-1] == '"':
|
||||||
|
return a[1:-1] # "значение" отдельным токеном
|
||||||
|
if a[0:1] == "/" and a[-1:] == '"' and '"' in a[:-1]:
|
||||||
|
i = a.index('"')
|
||||||
|
return a[:i] + a[i + 1:-1] # /N"имя" -> /Nимя
|
||||||
|
return a # File="…" не трогаем: там кавычки —
|
||||||
|
# часть синтаксиса строки соединения,
|
||||||
|
# и с ними на POSIX всё работает
|
||||||
|
cmd = [v8path] + [strip_framing_quotes(a) for a in arguments]
|
||||||
r = subprocess.run(cmd, input=b"", capture_output=True)
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
r.stdout = decode_platform_bytes(r.stdout)
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
r.stderr = decode_platform_bytes(r.stderr)
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
@@ -352,7 +368,7 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
|||||||
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
"""
|
"""
|
||||||
if warn_no_user and os.name == "nt" and not has_username:
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
sys.stderr.write(IBCMD_NOUSER_HINT)
|
sys.stdout.write(IBCMD_NOUSER_HINT)
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
r = subprocess.run(cmd, input=b"", capture_output=True)
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
r.stdout = decode_platform_bytes(r.stdout)
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
@@ -440,21 +456,21 @@ def main():
|
|||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if engine == "ibcmd":
|
if engine == "ibcmd":
|
||||||
if not args.InfoBasePath:
|
if not args.InfoBasePath:
|
||||||
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath)")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# --- Validate input file ---
|
# --- Validate input file ---
|
||||||
if not os.path.isfile(args.InputFile):
|
if not os.path.isfile(args.InputFile):
|
||||||
print(f"Error: input file not found: {args.InputFile}", file=sys.stderr)
|
print(f"Error: input file not found: {args.InputFile}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# --- ibcmd branch (file infobase only) ---
|
# --- ibcmd branch (file infobase only) ---
|
||||||
if engine == "ibcmd":
|
if engine == "ibcmd":
|
||||||
if args.AllExtensions:
|
if args.AllExtensions:
|
||||||
print("Error: ibcmd config load does not support -AllExtensions (use -Extension)", file=sys.stderr)
|
print("Error: ibcmd config load does not support -AllExtensions (use -Extension)")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
arguments = ["infobase", "config", "load", f"--db-path={args.InfoBasePath}"]
|
arguments = ["infobase", "config", "load", f"--db-path={args.InfoBasePath}"]
|
||||||
if args.Extension:
|
if args.Extension:
|
||||||
@@ -473,7 +489,7 @@ def main():
|
|||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
print(f"Configuration loaded successfully from: {args.InputFile}")
|
print(f"Configuration loaded successfully from: {args.InputFile}")
|
||||||
else:
|
else:
|
||||||
print(f"Error loading configuration (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
|
print(f"Error loading configuration (code: {result.returncode}){describe_exit(result.returncode)}")
|
||||||
sys.exit(result.returncode)
|
sys.exit(result.returncode)
|
||||||
|
|
||||||
# --- Temp dir ---
|
# --- Temp dir ---
|
||||||
@@ -517,7 +533,7 @@ def main():
|
|||||||
if exit_code == 0:
|
if exit_code == 0:
|
||||||
print(f"Configuration loaded successfully from: {args.InputFile}")
|
print(f"Configuration loaded successfully from: {args.InputFile}")
|
||||||
else:
|
else:
|
||||||
print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
|
print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}")
|
||||||
|
|
||||||
if os.path.isfile(out_file):
|
if os.path.isfile(out_file):
|
||||||
try:
|
try:
|
||||||
@@ -52,7 +52,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" <параметры>
|
python ".gemini/skills/db-load-dt/scripts/db-load-dt.py" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -82,10 +82,10 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" <п
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Файловая база
|
# Файловая база
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\base.dt"
|
python ".gemini/skills/db-load-dt/scripts/db-load-dt.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\base.dt"
|
||||||
|
|
||||||
# Серверная база с ускорением загрузки
|
# Серверная база с ускорением загрузки
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "base.dt" -JobsCount 4
|
python ".gemini/skills/db-load-dt/scripts/db-load-dt.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "base.dt" -JobsCount 4
|
||||||
```
|
```
|
||||||
|
|
||||||
## Связанные навыки
|
## Связанные навыки
|
||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
# db-load-dt v1.13 — Load 1C information base from DT file
|
# db-load-dt v1.16 — Load 1C information base from DT file
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
.\db-load-dt.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "backup.dt"
|
.\db-load-dt.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "backup.dt"
|
||||||
#>
|
#>
|
||||||
|
|
||||||
[CmdletBinding()]
|
[CmdletBinding(PositionalBinding=$false)]
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[string]$V8Path,
|
[string]$V8Path,
|
||||||
+32
-16
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-load-dt v1.13 — Load 1C information base from DT file
|
# db-load-dt v1.16 — Load 1C information base from DT file
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -120,7 +120,6 @@ def assert_extra_args(extra, engine, hints):
|
|||||||
print(
|
print(
|
||||||
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||||
f"({param} cannot extend the ibcmd command)",
|
f"({param} cannot extend the ibcmd command)",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
for k in owned:
|
for k in owned:
|
||||||
@@ -128,7 +127,6 @@ def assert_extra_args(extra, engine, hints):
|
|||||||
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||||
print(
|
print(
|
||||||
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@@ -196,14 +194,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
|||||||
print(
|
print(
|
||||||
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||||
"(use -AdditionalIbcmdArguments)",
|
"(use -AdditionalIbcmdArguments)",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if engine != "ibcmd" and ibcmd_extra:
|
if engine != "ibcmd" and ibcmd_extra:
|
||||||
print(
|
print(
|
||||||
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||||
"(use -AdditionalV8Arguments)",
|
"(use -AdditionalV8Arguments)",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if engine == "ibcmd":
|
if engine == "ibcmd":
|
||||||
@@ -245,14 +241,14 @@ def resolve_v8path(v8path):
|
|||||||
v8path = max(candidates, key=_version_key)
|
v8path = max(candidates, key=_version_key)
|
||||||
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
else:
|
else:
|
||||||
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
print("Error: 1C executable not found. Specify -V8Path")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
if os.path.isdir(v8path):
|
||||||
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
v8path = os.path.join(v8path, exe)
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
@@ -283,7 +279,7 @@ def assert_infobase_exists(path):
|
|||||||
if not path:
|
if not path:
|
||||||
return
|
return
|
||||||
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
|
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
|
||||||
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
|
print(f"Error: information base not found at {path} (no 1Cv8.1CD)")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
@@ -300,7 +296,7 @@ def clean_path(value, param=""):
|
|||||||
if len(v) > 3 and v[-1] in "\\/":
|
if len(v) > 3 and v[-1] in "\\/":
|
||||||
v = v[:-1]
|
v = v[:-1]
|
||||||
if '"' in v:
|
if '"' in v:
|
||||||
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
print(f"Error: {param or 'path'} contains a quote character: {value}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v
|
return v
|
||||||
|
|
||||||
@@ -319,11 +315,31 @@ def run_v8(v8path, arguments):
|
|||||||
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
|
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
|
||||||
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
|
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
|
||||||
escape those quotes, so there the command line is handed over ready-made.
|
escape those quotes, so there the command line is handed over ready-made.
|
||||||
|
|
||||||
|
На POSIX аргументы уходят СПИСКОМ, и кавычки, нужные для склейки на Windows, стали бы
|
||||||
|
частью значения: путь с пробелом платформа не находит («Неопределена информационная
|
||||||
|
база»), многословный -comment теряет молча. Поэтому здесь снимается ОДИН слой
|
||||||
|
обрамляющих кавычек. Склеенные ключи (/N"user", /ConfigurationRepositoryF"путь",
|
||||||
|
File="…") не задеты: у них кавычки внутри токена, а не по краям.
|
||||||
"""
|
"""
|
||||||
if os.name == "nt":
|
if os.name == "nt":
|
||||||
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||||
else:
|
else:
|
||||||
cmd = [v8path] + arguments
|
def strip_framing_quotes(a):
|
||||||
|
# Кавычки, которыми мы обрамляем значения ради склейки на Windows, на POSIX
|
||||||
|
# становятся ЧАСТЬЮ значения. Проверено на darwin: путь с пробелом отдельным
|
||||||
|
# токеном даёт «Неопределена информационная база», а склеенный
|
||||||
|
# /ConfigurationRepositoryF"путь с пробелом" — «завершилось с ошибкой»;
|
||||||
|
# без кавычек обе формы работают.
|
||||||
|
if len(a) > 1 and a[0] == '"' and a[-1] == '"':
|
||||||
|
return a[1:-1] # "значение" отдельным токеном
|
||||||
|
if a[0:1] == "/" and a[-1:] == '"' and '"' in a[:-1]:
|
||||||
|
i = a.index('"')
|
||||||
|
return a[:i] + a[i + 1:-1] # /N"имя" -> /Nимя
|
||||||
|
return a # File="…" не трогаем: там кавычки —
|
||||||
|
# часть синтаксиса строки соединения,
|
||||||
|
# и с ними на POSIX всё работает
|
||||||
|
cmd = [v8path] + [strip_framing_quotes(a) for a in arguments]
|
||||||
r = subprocess.run(cmd, input=b"", capture_output=True)
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
r.stdout = decode_platform_bytes(r.stdout)
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
r.stderr = decode_platform_bytes(r.stderr)
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
@@ -352,7 +368,7 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
|||||||
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
"""
|
"""
|
||||||
if warn_no_user and os.name == "nt" and not has_username:
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
sys.stderr.write(IBCMD_NOUSER_HINT)
|
sys.stdout.write(IBCMD_NOUSER_HINT)
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
r = subprocess.run(cmd, input=b"", capture_output=True)
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
r.stdout = decode_platform_bytes(r.stdout)
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
@@ -440,15 +456,15 @@ def main():
|
|||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if engine == "ibcmd":
|
if engine == "ibcmd":
|
||||||
if not args.InfoBasePath:
|
if not args.InfoBasePath:
|
||||||
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath)")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# --- Validate input file ---
|
# --- Validate input file ---
|
||||||
if not os.path.isfile(args.InputFile):
|
if not os.path.isfile(args.InputFile):
|
||||||
print(f"Error: input file not found: {args.InputFile}", file=sys.stderr)
|
print(f"Error: input file not found: {args.InputFile}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# --- ibcmd branch (file infobase only) ---
|
# --- ibcmd branch (file infobase only) ---
|
||||||
@@ -470,7 +486,7 @@ def main():
|
|||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
print(f"Information base restored successfully from: {args.InputFile}")
|
print(f"Information base restored successfully from: {args.InputFile}")
|
||||||
else:
|
else:
|
||||||
print(f"Error restoring information base (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
|
print(f"Error restoring information base (code: {result.returncode}){describe_exit(result.returncode)}")
|
||||||
sys.exit(result.returncode)
|
sys.exit(result.returncode)
|
||||||
|
|
||||||
# --- Temp dir ---
|
# --- Temp dir ---
|
||||||
@@ -512,7 +528,7 @@ def main():
|
|||||||
if exit_code == 0:
|
if exit_code == 0:
|
||||||
print(f"Information base restored successfully from: {args.InputFile}")
|
print(f"Information base restored successfully from: {args.InputFile}")
|
||||||
else:
|
else:
|
||||||
print(f"Error restoring information base (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
|
print(f"Error restoring information base (code: {exit_code}){describe_exit(exit_code)}")
|
||||||
|
|
||||||
if os.path.isfile(out_file):
|
if os.path.isfile(out_file):
|
||||||
try:
|
try:
|
||||||
@@ -38,7 +38,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" <параметры>
|
python ".gemini/skills/db-load-git/scripts/db-load-git.py" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -72,8 +72,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" <
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Все незафиксированные изменения
|
# Все незафиксированные изменения
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source All -UpdateDB
|
python ".gemini/skills/db-load-git/scripts/db-load-git.py" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source All -UpdateDB
|
||||||
|
|
||||||
# Из диапазона коммитов
|
# Из диапазона коммитов
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source Commit -CommitRange "HEAD~3..HEAD"
|
python ".gemini/skills/db-load-git/scripts/db-load-git.py" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source Commit -CommitRange "HEAD~3..HEAD"
|
||||||
```
|
```
|
||||||
+128
-4
@@ -1,4 +1,4 @@
|
|||||||
# db-load-git v1.20 — Load Git changes into 1C database
|
# db-load-git v1.26 — Load Git changes into 1C database
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
.\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -DryRun
|
.\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -DryRun
|
||||||
#>
|
#>
|
||||||
|
|
||||||
[CmdletBinding()]
|
[CmdletBinding(PositionalBinding=$false)]
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[string]$V8Path,
|
[string]$V8Path,
|
||||||
@@ -116,6 +116,15 @@ param(
|
|||||||
# но в логе есть отбраковка.
|
# но в логе есть отбраковка.
|
||||||
[switch]$StrictLog,
|
[switch]$StrictLog,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string]$RepositoryPath,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string]$RepositoryUser,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string]$RepositoryPassword,
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[string[]]$AdditionalV8Arguments = @(),
|
[string[]]$AdditionalV8Arguments = @(),
|
||||||
|
|
||||||
@@ -126,6 +135,115 @@ param(
|
|||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
# --- Реквизиты хранилища из .v8-project.json ---
|
||||||
|
# Модель их не передаёт: скрипт сопоставляет параметры соединения с записью в databases[]
|
||||||
|
# и берёт repository оттуда. Тот же приём, что в cf-edit.ps1 (сопоставление по configSrc).
|
||||||
|
function Find-V8Project([string]$startDir) {
|
||||||
|
$d = $startDir
|
||||||
|
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||||
|
$pj = Join-Path $d ".v8-project.json"
|
||||||
|
if (Test-Path $pj) { return $pj }
|
||||||
|
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||||
|
if ($parent -eq $d) { break }
|
||||||
|
$d = $parent
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
function Test-SamePath {
|
||||||
|
param([string]$A, [string]$B)
|
||||||
|
if (-not $A -or -not $B) { return $false }
|
||||||
|
try {
|
||||||
|
$na = [System.IO.Path]::GetFullPath($A).TrimEnd('\', '/')
|
||||||
|
$nb = [System.IO.Path]::GetFullPath($B).TrimEnd('\', '/')
|
||||||
|
return $na.Equals($nb, [System.StringComparison]::OrdinalIgnoreCase)
|
||||||
|
} catch { return $false }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Find-ProjectDatabase {
|
||||||
|
# Запись базы в реестре, соответствующая переданному соединению. $null, если не найдена.
|
||||||
|
$pf = Find-V8Project (Get-Location).Path
|
||||||
|
if (-not $pf) { return $null }
|
||||||
|
try { $proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json } catch { return $null }
|
||||||
|
if (-not $proj.databases) { return $null }
|
||||||
|
foreach ($db in $proj.databases) {
|
||||||
|
if ($InfoBasePath -and $db.path -and (Test-SamePath $db.path $InfoBasePath)) { return $db }
|
||||||
|
if ($InfoBaseServer -and $InfoBaseRef -and $db.server -and $db.ref) {
|
||||||
|
if ($db.server.Equals($InfoBaseServer, [System.StringComparison]::OrdinalIgnoreCase) -and
|
||||||
|
$db.ref.Equals($InfoBaseRef, [System.StringComparison]::OrdinalIgnoreCase)) { return $db }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-RepositorySettings {
|
||||||
|
# Возвращает @{ Path; User; Password; FromRegistry }. Явные -Repository* всегда сильнее реестра.
|
||||||
|
$dbRec = Find-ProjectDatabase
|
||||||
|
$rec = $null
|
||||||
|
if ($dbRec) {
|
||||||
|
if ($Extension) {
|
||||||
|
# У расширения СВОЁ хранилище со своим путём (проверено): выбирается парой
|
||||||
|
# /ConfigurationRepositoryF"<путь расширения>" + -Extension "<Имя>".
|
||||||
|
if ($dbRec.extensions) {
|
||||||
|
foreach ($ext in $dbRec.extensions) {
|
||||||
|
if ($ext.name -and $ext.name.Equals($Extension, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||||
|
$rec = $ext.repository
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$rec = $dbRec.repository
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$path = if ($RepositoryPath) { $RepositoryPath } elseif ($rec -and $rec.path) { [string]$rec.path } else { $null }
|
||||||
|
$user = if ($RepositoryUser) { $RepositoryUser } elseif ($rec -and $rec.user) { [string]$rec.user } else { $null }
|
||||||
|
# Пустой пароль = отсутствующий: 1С требует опускать ключ целиком, а не передавать пустое значение.
|
||||||
|
$pwd = if ($RepositoryPassword) { $RepositoryPassword } elseif ($rec -and $rec.password) { [string]$rec.password } else { $null }
|
||||||
|
return @{
|
||||||
|
Path = if ($path) { $path.Trim().Trim('"') } else { $null }
|
||||||
|
User = $user
|
||||||
|
Password = $pwd
|
||||||
|
FromRegistry = [bool]($rec -and $rec.path)
|
||||||
|
DbRecord = $dbRec
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-RepositoryArgs {
|
||||||
|
# Ключи доступа к хранилищу. Форма — кавычки ВНУТРИ токена, как у /N и /P.
|
||||||
|
param([hashtable]$Repo)
|
||||||
|
$a = @()
|
||||||
|
if (-not $Repo -or -not $Repo.Path) { return $a }
|
||||||
|
$a += "/ConfigurationRepositoryF`"$($Repo.Path)`""
|
||||||
|
if ($Repo.User) { $a += "/ConfigurationRepositoryN`"$($Repo.User)`"" }
|
||||||
|
if ($Repo.Password) { $a += "/ConfigurationRepositoryP`"$($Repo.Password)`"" }
|
||||||
|
return $a
|
||||||
|
}
|
||||||
|
|
||||||
|
# Сообщения платформы про хранилище конфигурации называют причину, но не действие. Действие
|
||||||
|
# дописываем сами: без него модель упирается в отказ и не знает, чем его лечить.
|
||||||
|
function Write-RepositoryHints {
|
||||||
|
param([string]$LogText)
|
||||||
|
if (-not $LogText) { return }
|
||||||
|
if ($LogText -match 'текущая конфигурация помещена в хранилище') {
|
||||||
|
Write-Host "[hint] полная загрузка в базу, подключённую к хранилищу, невозможна." -ForegroundColor Yellow
|
||||||
|
Write-Host " Используйте -Mode Partial, предварительно захватив объекты: /db-repo lock" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
foreach ($m in [regex]::Matches($LogText, 'объект метаданных ([^\s]+) не захвачен в хранилище')) {
|
||||||
|
$obj = $m.Groups[1].Value
|
||||||
|
if ($obj -eq 'Configuration') {
|
||||||
|
Write-Host "[hint] не захвачен корень конфигурации — он нужен, чтобы добавить или удалить объект:" -ForegroundColor Yellow
|
||||||
|
Write-Host " /db-repo lock <база> -Objects `"Конфигурация`"" -ForegroundColor Yellow
|
||||||
|
} else {
|
||||||
|
Write-Host "[hint] объект не захвачен в хранилище: /db-repo lock <база> -Objects `"$obj`"" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($LogText -match 'Соединение с хранилищем конфигурации не установлено') {
|
||||||
|
Write-Host "[hint] соединение с хранилищем не установлено. Две причины:" -ForegroundColor Yellow
|
||||||
|
Write-Host " реквизиты неизвестны — добавьте `"repository`" в запись базы в .v8-project.json (см. /db-list);" -ForegroundColor Yellow
|
||||||
|
Write-Host " либо хранилище недоступно — для сетевого проверьте сервер хранилища и порт." -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function Protect-Secrets {
|
function Protect-Secrets {
|
||||||
# Redact literal secret values from a display string (String.Replace is literal, not regex).
|
# Redact literal secret values from a display string (String.Replace is literal, not regex).
|
||||||
param([string]$Text, [string[]]$Secrets)
|
param([string]$Text, [string[]]$Secrets)
|
||||||
@@ -147,7 +265,7 @@ $script:IbcmdOwnedKeys = @(
|
|||||||
'--import', '--export', '--apply', '--force', '--create-database',
|
'--import', '--export', '--apply', '--force', '--create-database',
|
||||||
'--user', '--password'
|
'--user', '--password'
|
||||||
)
|
)
|
||||||
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
|
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP', '/ConfigurationRepositoryP')
|
||||||
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
|
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
|
||||||
|
|
||||||
function Test-ArgKeyMatch {
|
function Test-ArgKeyMatch {
|
||||||
@@ -668,6 +786,11 @@ try {
|
|||||||
if ($UserName) { $arguments += "/N`"$UserName`"" }
|
if ($UserName) { $arguments += "/N`"$UserName`"" }
|
||||||
if ($Password) { $arguments += "/P`"$Password`"" }
|
if ($Password) { $arguments += "/P`"$Password`"" }
|
||||||
|
|
||||||
|
# База под хранилищем не примет НИ ОДНОЙ операции конфигуратора без этих реквизитов, а для
|
||||||
|
# базы вне хранилища они безвредны — поэтому подставляем всегда, когда они известны.
|
||||||
|
$__repo = Resolve-RepositorySettings
|
||||||
|
$arguments += Get-RepositoryArgs $__repo
|
||||||
|
|
||||||
$arguments += "/LoadConfigFromFiles", "`"$ConfigDir`""
|
$arguments += "/LoadConfigFromFiles", "`"$ConfigDir`""
|
||||||
$arguments += "-listFile", "`"$listFile`""
|
$arguments += "-listFile", "`"$listFile`""
|
||||||
$arguments += "-Format", $Format
|
$arguments += "-Format", $Format
|
||||||
@@ -695,7 +818,7 @@ try {
|
|||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "Executing partial configuration load..."
|
Write-Host "Executing partial configuration load..."
|
||||||
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName, $__repo.Password))"
|
||||||
|
|
||||||
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
|
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
|
||||||
$exitCode = $__v8.ExitCode
|
$exitCode = $__v8.ExitCode
|
||||||
@@ -718,6 +841,7 @@ try {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Write-PlatformOutput $__v8.Output
|
Write-PlatformOutput $__v8.Output
|
||||||
|
Write-RepositoryHints $logContent
|
||||||
|
|
||||||
# Причину не называем: строки лога печатаются следом и говорят за себя, а класс проблемы
|
# Причину не называем: строки лога печатаются следом и говорят за себя, а класс проблемы
|
||||||
# разный — от отброшенного свойства до нерабочей на этой платформе конфигурации. Подсказку
|
# разный — от отброшенного свойства до нерабочей на этой платформе конфигурации. Подсказку
|
||||||
+157
-26
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-load-git v1.20 — Load Git changes into 1C database
|
# db-load-git v1.26 — Load Git changes into 1C database
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -72,10 +72,116 @@ IBCMD_OWNED_KEYS = [
|
|||||||
"--import", "--export", "--apply", "--force", "--create-database",
|
"--import", "--export", "--apply", "--force", "--create-database",
|
||||||
"--user", "--password",
|
"--user", "--password",
|
||||||
]
|
]
|
||||||
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
|
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP", "/ConfigurationRepositoryP"]
|
||||||
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
|
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
|
||||||
|
|
||||||
|
|
||||||
|
# --- Реквизиты хранилища из .v8-project.json ---
|
||||||
|
# Модель их не передаёт: скрипт сопоставляет параметры соединения с записью в databases[]
|
||||||
|
# и берёт repository оттуда. Тот же приём, что в cf-edit.py (сопоставление по configSrc).
|
||||||
|
def _sg_find_v8project(start_dir):
|
||||||
|
d = start_dir
|
||||||
|
for _ in range(20):
|
||||||
|
if not d:
|
||||||
|
break
|
||||||
|
pj = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pj):
|
||||||
|
return pj
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
break
|
||||||
|
d = parent
|
||||||
|
return None
|
||||||
|
|
||||||
|
def same_path(a, b):
|
||||||
|
if not a or not b:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
return os.path.abspath(a).rstrip("\\/").lower() == os.path.abspath(b).rstrip("\\/").lower()
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def find_project_database(args):
|
||||||
|
"""Запись базы в реестре, соответствующая переданному соединению. None, если не найдена."""
|
||||||
|
pf = _sg_find_v8project(os.getcwd())
|
||||||
|
if not pf:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
proj = json.load(f)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
for db in proj.get("databases") or []:
|
||||||
|
if args.InfoBasePath and db.get("path") and same_path(db["path"], args.InfoBasePath):
|
||||||
|
return db
|
||||||
|
if args.InfoBaseServer and args.InfoBaseRef and db.get("server") and db.get("ref"):
|
||||||
|
if (db["server"].lower() == args.InfoBaseServer.lower()
|
||||||
|
and db["ref"].lower() == args.InfoBaseRef.lower()):
|
||||||
|
return db
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_repository_settings(args):
|
||||||
|
"""Возвращает dict path/user/password/from_registry. Явные -Repository* сильнее реестра."""
|
||||||
|
db_rec = find_project_database(args)
|
||||||
|
rec = None
|
||||||
|
if db_rec:
|
||||||
|
if args.Extension:
|
||||||
|
# У расширения СВОЁ хранилище со своим путём (проверено): выбирается парой
|
||||||
|
# /ConfigurationRepositoryF"<путь расширения>" + -Extension "<Имя>".
|
||||||
|
for ext in db_rec.get("extensions") or []:
|
||||||
|
if (ext.get("name") or "").lower() == args.Extension.lower():
|
||||||
|
rec = ext.get("repository")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
rec = db_rec.get("repository")
|
||||||
|
path = args.RepositoryPath or ((rec or {}).get("path") or None)
|
||||||
|
user = args.RepositoryUser or ((rec or {}).get("user") or None)
|
||||||
|
# Пустой пароль = отсутствующий: 1С требует опускать ключ целиком, а не передавать пустое значение.
|
||||||
|
pwd = args.RepositoryPassword or ((rec or {}).get("password") or None)
|
||||||
|
return {
|
||||||
|
"path": path.strip().strip('"') if path else None,
|
||||||
|
"user": user,
|
||||||
|
"password": pwd,
|
||||||
|
"from_registry": bool(rec and rec.get("path")),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def repository_args(repo):
|
||||||
|
"""Ключи доступа к хранилищу. Форма — кавычки ВНУТРИ токена, как у /N и /P."""
|
||||||
|
a = []
|
||||||
|
if not repo or not repo.get("path"):
|
||||||
|
return a
|
||||||
|
a.append('/ConfigurationRepositoryF"%s"' % repo["path"])
|
||||||
|
if repo.get("user"):
|
||||||
|
a.append('/ConfigurationRepositoryN"%s"' % repo["user"])
|
||||||
|
if repo.get("password"):
|
||||||
|
a.append('/ConfigurationRepositoryP"%s"' % repo["password"])
|
||||||
|
return a
|
||||||
|
|
||||||
|
|
||||||
|
# Сообщения платформы про хранилище конфигурации называют причину, но не действие. Действие
|
||||||
|
# дописываем сами: без него модель упирается в отказ и не знает, чем его лечить.
|
||||||
|
def write_repository_hints(log_text):
|
||||||
|
if not log_text:
|
||||||
|
return
|
||||||
|
if "текущая конфигурация помещена в хранилище" in log_text:
|
||||||
|
print("[hint] полная загрузка в базу, подключённую к хранилищу, невозможна.")
|
||||||
|
print(" Используйте -Mode Partial, предварительно захватив объекты: /db-repo lock")
|
||||||
|
for m in re.finditer(r"объект метаданных (\S+) не захвачен в хранилище", log_text):
|
||||||
|
obj = m.group(1)
|
||||||
|
if obj == "Configuration":
|
||||||
|
print("[hint] не захвачен корень конфигурации — он нужен, чтобы добавить или удалить объект:")
|
||||||
|
print(' /db-repo lock <база> -Objects "Конфигурация"')
|
||||||
|
else:
|
||||||
|
print('[hint] объект не захвачен в хранилище: /db-repo lock <база> -Objects "%s"' % obj)
|
||||||
|
if "Соединение с хранилищем конфигурации не установлено" in log_text:
|
||||||
|
print("[hint] соединение с хранилищем не установлено. Две причины:")
|
||||||
|
print(' реквизиты неизвестны — добавьте "repository" в запись базы в .v8-project.json (см. /db-list);')
|
||||||
|
print(" либо хранилище недоступно — для сетевого проверьте сервер хранилища и порт.")
|
||||||
|
|
||||||
|
|
||||||
def arg_key_match(token, key):
|
def arg_key_match(token, key):
|
||||||
"""Token matches a key when it equals it, or starts with it and the next character
|
"""Token matches a key when it equals it, or starts with it and the next character
|
||||||
is not a letter — catches glued /N"user" and --password=x, while keeping
|
is not a letter — catches glued /N"user" and --password=x, while keeping
|
||||||
@@ -120,7 +226,6 @@ def assert_extra_args(extra, engine, hints):
|
|||||||
print(
|
print(
|
||||||
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||||
f"({param} cannot extend the ibcmd command)",
|
f"({param} cannot extend the ibcmd command)",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
for k in owned:
|
for k in owned:
|
||||||
@@ -128,7 +233,6 @@ def assert_extra_args(extra, engine, hints):
|
|||||||
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||||
print(
|
print(
|
||||||
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@@ -196,14 +300,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
|||||||
print(
|
print(
|
||||||
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||||
"(use -AdditionalIbcmdArguments)",
|
"(use -AdditionalIbcmdArguments)",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if engine != "ibcmd" and ibcmd_extra:
|
if engine != "ibcmd" and ibcmd_extra:
|
||||||
print(
|
print(
|
||||||
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||||
"(use -AdditionalV8Arguments)",
|
"(use -AdditionalV8Arguments)",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if engine == "ibcmd":
|
if engine == "ibcmd":
|
||||||
@@ -245,14 +347,14 @@ def resolve_v8path(v8path):
|
|||||||
v8path = max(candidates, key=_version_key)
|
v8path = max(candidates, key=_version_key)
|
||||||
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
else:
|
else:
|
||||||
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
print("Error: 1C executable not found. Specify -V8Path")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
if os.path.isdir(v8path):
|
||||||
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
v8path = os.path.join(v8path, exe)
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
@@ -283,7 +385,7 @@ def assert_infobase_exists(path):
|
|||||||
if not path:
|
if not path:
|
||||||
return
|
return
|
||||||
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
|
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
|
||||||
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
|
print(f"Error: information base not found at {path} (no 1Cv8.1CD)")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
@@ -300,7 +402,7 @@ def clean_path(value, param=""):
|
|||||||
if len(v) > 3 and v[-1] in "\\/":
|
if len(v) > 3 and v[-1] in "\\/":
|
||||||
v = v[:-1]
|
v = v[:-1]
|
||||||
if '"' in v:
|
if '"' in v:
|
||||||
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
print(f"Error: {param or 'path'} contains a quote character: {value}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v
|
return v
|
||||||
|
|
||||||
@@ -319,11 +421,31 @@ def run_v8(v8path, arguments):
|
|||||||
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
|
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
|
||||||
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
|
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
|
||||||
escape those quotes, so there the command line is handed over ready-made.
|
escape those quotes, so there the command line is handed over ready-made.
|
||||||
|
|
||||||
|
На POSIX аргументы уходят СПИСКОМ, и кавычки, нужные для склейки на Windows, стали бы
|
||||||
|
частью значения: путь с пробелом платформа не находит («Неопределена информационная
|
||||||
|
база»), многословный -comment теряет молча. Поэтому здесь снимается ОДИН слой
|
||||||
|
обрамляющих кавычек. Склеенные ключи (/N"user", /ConfigurationRepositoryF"путь",
|
||||||
|
File="…") не задеты: у них кавычки внутри токена, а не по краям.
|
||||||
"""
|
"""
|
||||||
if os.name == "nt":
|
if os.name == "nt":
|
||||||
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||||
else:
|
else:
|
||||||
cmd = [v8path] + arguments
|
def strip_framing_quotes(a):
|
||||||
|
# Кавычки, которыми мы обрамляем значения ради склейки на Windows, на POSIX
|
||||||
|
# становятся ЧАСТЬЮ значения. Проверено на darwin: путь с пробелом отдельным
|
||||||
|
# токеном даёт «Неопределена информационная база», а склеенный
|
||||||
|
# /ConfigurationRepositoryF"путь с пробелом" — «завершилось с ошибкой»;
|
||||||
|
# без кавычек обе формы работают.
|
||||||
|
if len(a) > 1 and a[0] == '"' and a[-1] == '"':
|
||||||
|
return a[1:-1] # "значение" отдельным токеном
|
||||||
|
if a[0:1] == "/" and a[-1:] == '"' and '"' in a[:-1]:
|
||||||
|
i = a.index('"')
|
||||||
|
return a[:i] + a[i + 1:-1] # /N"имя" -> /Nимя
|
||||||
|
return a # File="…" не трогаем: там кавычки —
|
||||||
|
# часть синтаксиса строки соединения,
|
||||||
|
# и с ними на POSIX всё работает
|
||||||
|
cmd = [v8path] + [strip_framing_quotes(a) for a in arguments]
|
||||||
r = subprocess.run(cmd, input=b"", capture_output=True)
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
r.stdout = decode_platform_bytes(r.stdout)
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
r.stderr = decode_platform_bytes(r.stderr)
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
@@ -384,7 +506,7 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
|||||||
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
"""
|
"""
|
||||||
if warn_no_user and os.name == "nt" and not has_username:
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
sys.stderr.write(IBCMD_NOUSER_HINT)
|
sys.stdout.write(IBCMD_NOUSER_HINT)
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
r = subprocess.run(cmd, input=b"", capture_output=True)
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
r.stdout = decode_platform_bytes(r.stdout)
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
@@ -460,6 +582,9 @@ def main():
|
|||||||
parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server")
|
parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server")
|
||||||
parser.add_argument("-UserName", default="", help="1C user name")
|
parser.add_argument("-UserName", default="", help="1C user name")
|
||||||
parser.add_argument("-Password", default="", help="1C user password")
|
parser.add_argument("-Password", default="", help="1C user password")
|
||||||
|
parser.add_argument("-RepositoryPath", default="")
|
||||||
|
parser.add_argument("-RepositoryUser", default="")
|
||||||
|
parser.add_argument("-RepositoryPassword", default="")
|
||||||
parser.add_argument("-ConfigDir", required=True, help="Directory with XML configuration (git repo)")
|
parser.add_argument("-ConfigDir", required=True, help="Directory with XML configuration (git repo)")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-Source",
|
"-Source",
|
||||||
@@ -506,10 +631,10 @@ def main():
|
|||||||
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
if engine == "ibcmd":
|
if engine == "ibcmd":
|
||||||
if not args.InfoBasePath:
|
if not args.InfoBasePath:
|
||||||
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath)")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# --- Resolve additional arguments for the selected engine ---
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
@@ -526,19 +651,19 @@ def main():
|
|||||||
|
|
||||||
# --- Validate config dir ---
|
# --- Validate config dir ---
|
||||||
if not os.path.exists(args.ConfigDir):
|
if not os.path.exists(args.ConfigDir):
|
||||||
print(f"Error: config directory not found: {args.ConfigDir}", file=sys.stderr)
|
print(f"Error: config directory not found: {args.ConfigDir}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# --- Validate Commit mode ---
|
# --- Validate Commit mode ---
|
||||||
if args.Source == "Commit" and not args.CommitRange:
|
if args.Source == "Commit" and not args.CommitRange:
|
||||||
print("Error: -CommitRange required for Source=Commit", file=sys.stderr)
|
print("Error: -CommitRange required for Source=Commit")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# --- Check git ---
|
# --- Check git ---
|
||||||
try:
|
try:
|
||||||
subprocess.run(["git", "--version"], capture_output=True, text=True, check=True)
|
subprocess.run(["git", "--version"], capture_output=True, text=True, check=True)
|
||||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||||
print("Error: git not found in PATH", file=sys.stderr)
|
print("Error: git not found in PATH")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# --- Get changed files from Git ---
|
# --- Get changed files from Git ---
|
||||||
@@ -617,10 +742,10 @@ def main():
|
|||||||
config_files.append(rel_path)
|
config_files.append(rel_path)
|
||||||
|
|
||||||
if support_skipped:
|
if support_skipped:
|
||||||
print("[ВНИМАНИЕ] Состояние поддержки изменено в коммите, но частично не загружается (исключено):", file=sys.stderr)
|
print("[ВНИМАНИЕ] Состояние поддержки изменено в коммите, но частично не загружается (исключено):")
|
||||||
for sf in support_skipped:
|
for sf in support_skipped:
|
||||||
print(f" - {sf}", file=sys.stderr)
|
print(f" - {sf}")
|
||||||
print(" Смена состояния поддержки применяется только полной загрузкой (db-load-xml -Mode Full).", file=sys.stderr)
|
print(" Смена состояния поддержки применяется только полной загрузкой (db-load-xml -Mode Full).")
|
||||||
|
|
||||||
if len(config_files) == 0:
|
if len(config_files) == 0:
|
||||||
print("No configuration files found in changes")
|
print("No configuration files found in changes")
|
||||||
@@ -644,10 +769,10 @@ def main():
|
|||||||
if engine == "ibcmd":
|
if engine == "ibcmd":
|
||||||
# --- ibcmd branch (file infobase only; import specific files) ---
|
# --- ibcmd branch (file infobase only; import specific files) ---
|
||||||
if args.Format == "Plain":
|
if args.Format == "Plain":
|
||||||
print("Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)", file=sys.stderr)
|
print("Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if args.AllExtensions:
|
if args.AllExtensions:
|
||||||
print("Error: ibcmd config import does not support -AllExtensions (use -Extension or 1cv8)", file=sys.stderr)
|
print("Error: ibcmd config import does not support -AllExtensions (use -Extension or 1cv8)")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
arguments = ["infobase", "config", "import", "files"] + config_files
|
arguments = ["infobase", "config", "import", "files"] + config_files
|
||||||
arguments += [f"--base-dir={args.ConfigDir}", f"--db-path={args.InfoBasePath}"]
|
arguments += [f"--base-dir={args.ConfigDir}", f"--db-path={args.InfoBasePath}"]
|
||||||
@@ -664,7 +789,7 @@ def main():
|
|||||||
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||||
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
print(f"Error loading changes (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
|
print(f"Error loading changes (code: {result.returncode}){describe_exit(result.returncode)}")
|
||||||
sys.exit(result.returncode)
|
sys.exit(result.returncode)
|
||||||
print(f"Changes loaded successfully ({len(config_files)} files)")
|
print(f"Changes loaded successfully ({len(config_files)} files)")
|
||||||
exit_code = 0
|
exit_code = 0
|
||||||
@@ -682,7 +807,7 @@ def main():
|
|||||||
if exit_code == 0:
|
if exit_code == 0:
|
||||||
print("Database configuration updated successfully")
|
print("Database configuration updated successfully")
|
||||||
else:
|
else:
|
||||||
print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
|
print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}")
|
||||||
print_platform_output(ar)
|
print_platform_output(ar)
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
@@ -704,6 +829,11 @@ def main():
|
|||||||
if args.Password:
|
if args.Password:
|
||||||
arguments.append(f'/P"{args.Password}"')
|
arguments.append(f'/P"{args.Password}"')
|
||||||
|
|
||||||
|
# База под хранилищем не примет НИ ОДНОЙ операции конфигуратора без этих реквизитов, а для
|
||||||
|
# базы вне хранилища они безвредны — поэтому подставляем всегда, когда они известны.
|
||||||
|
repo = resolve_repository_settings(args)
|
||||||
|
arguments.extend(repository_args(repo))
|
||||||
|
|
||||||
arguments += ["/LoadConfigFromFiles", f'"{args.ConfigDir}"']
|
arguments += ["/LoadConfigFromFiles", f'"{args.ConfigDir}"']
|
||||||
arguments += ["-listFile", f'"{list_file}"']
|
arguments += ["-listFile", f'"{list_file}"']
|
||||||
arguments += ["-Format", args.Format]
|
arguments += ["-Format", args.Format]
|
||||||
@@ -729,7 +859,7 @@ def main():
|
|||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
print("")
|
print("")
|
||||||
print("Executing partial configuration load...")
|
print("Executing partial configuration load...")
|
||||||
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName, repo['password'])}")
|
||||||
|
|
||||||
result = run_v8(v8path, arguments)
|
result = run_v8(v8path, arguments)
|
||||||
exit_code = result.returncode
|
exit_code = result.returncode
|
||||||
@@ -739,7 +869,7 @@ def main():
|
|||||||
if exit_code == 0:
|
if exit_code == 0:
|
||||||
print("Load completed successfully")
|
print("Load completed successfully")
|
||||||
else:
|
else:
|
||||||
print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
|
print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}")
|
||||||
|
|
||||||
log_content = ""
|
log_content = ""
|
||||||
if os.path.isfile(out_file):
|
if os.path.isfile(out_file):
|
||||||
@@ -754,6 +884,7 @@ def main():
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
print_platform_output(result)
|
print_platform_output(result)
|
||||||
|
write_repository_hints(log_content)
|
||||||
|
|
||||||
# Причину не называем: строки лога печатаются следом и говорят за себя, а класс проблемы
|
# Причину не называем: строки лога печатаются следом и говорят за себя, а класс проблемы
|
||||||
# разный — от отброшенного свойства до нерабочей на этой платформе конфигурации. Подсказку
|
# разный — от отброшенного свойства до нерабочей на этой платформе конфигурации. Подсказку
|
||||||
@@ -34,11 +34,12 @@ allowed-tools:
|
|||||||
Если файла нет — предложи `/db-list add`.
|
Если файла нет — предложи `/db-list add`.
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
Если в записи базы указан `configSrc` — используй как каталог загрузки по умолчанию.
|
Если в записи базы указан `configSrc` — используй как каталог загрузки по умолчанию.
|
||||||
|
Для `-Extension` каталог берётся из `extensions[].src` записи базы, если он там указан.
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" <параметры>
|
python ".gemini/skills/db-load-xml/scripts/db-load-xml.py" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -90,14 +91,14 @@ Documents/Заказ/Forms/ФормаДокумента.xml
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Полная загрузка
|
# Полная загрузка
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full
|
python ".gemini/skills/db-load-xml/scripts/db-load-xml.py" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full
|
||||||
|
|
||||||
# Частичная загрузка конкретных файлов
|
# Частичная загрузка конкретных файлов
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl"
|
python ".gemini/skills/db-load-xml/scripts/db-load-xml.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl"
|
||||||
|
|
||||||
# Загрузка расширения
|
# Загрузка расширения
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
|
python ".gemini/skills/db-load-xml/scripts/db-load-xml.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
|
||||||
|
|
||||||
# Загрузка + обновление БД в одном запуске
|
# Загрузка + обновление БД в одном запуске
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full -UpdateDB
|
python ".gemini/skills/db-load-xml/scripts/db-load-xml.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full -UpdateDB
|
||||||
```
|
```
|
||||||
+143
-7
@@ -1,4 +1,4 @@
|
|||||||
# db-load-xml v1.21 — Load 1C configuration from XML files
|
# db-load-xml v1.28 — Load 1C configuration from XML files
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
.\db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl"
|
.\db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl"
|
||||||
#>
|
#>
|
||||||
|
|
||||||
[CmdletBinding()]
|
[CmdletBinding(PositionalBinding=$false)]
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[string]$V8Path,
|
[string]$V8Path,
|
||||||
@@ -85,8 +85,10 @@ param(
|
|||||||
[string]$ConfigDir,
|
[string]$ConfigDir,
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[ValidateSet("Full", "Partial")]
|
# Пустое значение = режим не задан. Прежнее умолчание Full подставляется ниже, после того
|
||||||
[string]$Mode = "Full",
|
# как станет видно, перечислены ли файлы.
|
||||||
|
[ValidateSet("", "Full", "Partial")]
|
||||||
|
[string]$Mode = "",
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[string]$Files,
|
[string]$Files,
|
||||||
@@ -110,6 +112,15 @@ param(
|
|||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[switch]$StrictLog,
|
[switch]$StrictLog,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string]$RepositoryPath,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string]$RepositoryUser,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string]$RepositoryPassword,
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[string[]]$AdditionalV8Arguments = @(),
|
[string[]]$AdditionalV8Arguments = @(),
|
||||||
|
|
||||||
@@ -120,6 +131,115 @@ param(
|
|||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
# --- Реквизиты хранилища из .v8-project.json ---
|
||||||
|
# Модель их не передаёт: скрипт сопоставляет параметры соединения с записью в databases[]
|
||||||
|
# и берёт repository оттуда. Тот же приём, что в cf-edit.ps1 (сопоставление по configSrc).
|
||||||
|
function Find-V8Project([string]$startDir) {
|
||||||
|
$d = $startDir
|
||||||
|
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||||
|
$pj = Join-Path $d ".v8-project.json"
|
||||||
|
if (Test-Path $pj) { return $pj }
|
||||||
|
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||||
|
if ($parent -eq $d) { break }
|
||||||
|
$d = $parent
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
function Test-SamePath {
|
||||||
|
param([string]$A, [string]$B)
|
||||||
|
if (-not $A -or -not $B) { return $false }
|
||||||
|
try {
|
||||||
|
$na = [System.IO.Path]::GetFullPath($A).TrimEnd('\', '/')
|
||||||
|
$nb = [System.IO.Path]::GetFullPath($B).TrimEnd('\', '/')
|
||||||
|
return $na.Equals($nb, [System.StringComparison]::OrdinalIgnoreCase)
|
||||||
|
} catch { return $false }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Find-ProjectDatabase {
|
||||||
|
# Запись базы в реестре, соответствующая переданному соединению. $null, если не найдена.
|
||||||
|
$pf = Find-V8Project (Get-Location).Path
|
||||||
|
if (-not $pf) { return $null }
|
||||||
|
try { $proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json } catch { return $null }
|
||||||
|
if (-not $proj.databases) { return $null }
|
||||||
|
foreach ($db in $proj.databases) {
|
||||||
|
if ($InfoBasePath -and $db.path -and (Test-SamePath $db.path $InfoBasePath)) { return $db }
|
||||||
|
if ($InfoBaseServer -and $InfoBaseRef -and $db.server -and $db.ref) {
|
||||||
|
if ($db.server.Equals($InfoBaseServer, [System.StringComparison]::OrdinalIgnoreCase) -and
|
||||||
|
$db.ref.Equals($InfoBaseRef, [System.StringComparison]::OrdinalIgnoreCase)) { return $db }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-RepositorySettings {
|
||||||
|
# Возвращает @{ Path; User; Password; FromRegistry }. Явные -Repository* всегда сильнее реестра.
|
||||||
|
$dbRec = Find-ProjectDatabase
|
||||||
|
$rec = $null
|
||||||
|
if ($dbRec) {
|
||||||
|
if ($Extension) {
|
||||||
|
# У расширения СВОЁ хранилище со своим путём (проверено): выбирается парой
|
||||||
|
# /ConfigurationRepositoryF"<путь расширения>" + -Extension "<Имя>".
|
||||||
|
if ($dbRec.extensions) {
|
||||||
|
foreach ($ext in $dbRec.extensions) {
|
||||||
|
if ($ext.name -and $ext.name.Equals($Extension, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||||
|
$rec = $ext.repository
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$rec = $dbRec.repository
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$path = if ($RepositoryPath) { $RepositoryPath } elseif ($rec -and $rec.path) { [string]$rec.path } else { $null }
|
||||||
|
$user = if ($RepositoryUser) { $RepositoryUser } elseif ($rec -and $rec.user) { [string]$rec.user } else { $null }
|
||||||
|
# Пустой пароль = отсутствующий: 1С требует опускать ключ целиком, а не передавать пустое значение.
|
||||||
|
$pwd = if ($RepositoryPassword) { $RepositoryPassword } elseif ($rec -and $rec.password) { [string]$rec.password } else { $null }
|
||||||
|
return @{
|
||||||
|
Path = if ($path) { $path.Trim().Trim('"') } else { $null }
|
||||||
|
User = $user
|
||||||
|
Password = $pwd
|
||||||
|
FromRegistry = [bool]($rec -and $rec.path)
|
||||||
|
DbRecord = $dbRec
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-RepositoryArgs {
|
||||||
|
# Ключи доступа к хранилищу. Форма — кавычки ВНУТРИ токена, как у /N и /P.
|
||||||
|
param([hashtable]$Repo)
|
||||||
|
$a = @()
|
||||||
|
if (-not $Repo -or -not $Repo.Path) { return $a }
|
||||||
|
$a += "/ConfigurationRepositoryF`"$($Repo.Path)`""
|
||||||
|
if ($Repo.User) { $a += "/ConfigurationRepositoryN`"$($Repo.User)`"" }
|
||||||
|
if ($Repo.Password) { $a += "/ConfigurationRepositoryP`"$($Repo.Password)`"" }
|
||||||
|
return $a
|
||||||
|
}
|
||||||
|
|
||||||
|
# Сообщения платформы про хранилище конфигурации называют причину, но не действие. Действие
|
||||||
|
# дописываем сами: без него модель упирается в отказ и не знает, чем его лечить.
|
||||||
|
function Write-RepositoryHints {
|
||||||
|
param([string]$LogText)
|
||||||
|
if (-not $LogText) { return }
|
||||||
|
if ($LogText -match 'текущая конфигурация помещена в хранилище') {
|
||||||
|
Write-Host "[hint] полная загрузка в базу, подключённую к хранилищу, невозможна." -ForegroundColor Yellow
|
||||||
|
Write-Host " Используйте -Mode Partial, предварительно захватив объекты: /db-repo lock" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
foreach ($m in [regex]::Matches($LogText, 'объект метаданных ([^\s]+) не захвачен в хранилище')) {
|
||||||
|
$obj = $m.Groups[1].Value
|
||||||
|
if ($obj -eq 'Configuration') {
|
||||||
|
Write-Host "[hint] не захвачен корень конфигурации — он нужен, чтобы добавить или удалить объект:" -ForegroundColor Yellow
|
||||||
|
Write-Host " /db-repo lock <база> -Objects `"Конфигурация`"" -ForegroundColor Yellow
|
||||||
|
} else {
|
||||||
|
Write-Host "[hint] объект не захвачен в хранилище: /db-repo lock <база> -Objects `"$obj`"" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($LogText -match 'Соединение с хранилищем конфигурации не установлено') {
|
||||||
|
Write-Host "[hint] соединение с хранилищем не установлено. Две причины:" -ForegroundColor Yellow
|
||||||
|
Write-Host " реквизиты неизвестны — добавьте `"repository`" в запись базы в .v8-project.json (см. /db-list);" -ForegroundColor Yellow
|
||||||
|
Write-Host " либо хранилище недоступно — для сетевого проверьте сервер хранилища и порт." -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function Protect-Secrets {
|
function Protect-Secrets {
|
||||||
# Redact literal secret values from a display string (String.Replace is literal, not regex).
|
# Redact literal secret values from a display string (String.Replace is literal, not regex).
|
||||||
param([string]$Text, [string[]]$Secrets)
|
param([string]$Text, [string[]]$Secrets)
|
||||||
@@ -158,7 +278,7 @@ $script:IbcmdOwnedKeys = @(
|
|||||||
'--import', '--export', '--apply', '--force', '--create-database',
|
'--import', '--export', '--apply', '--force', '--create-database',
|
||||||
'--user', '--password'
|
'--user', '--password'
|
||||||
)
|
)
|
||||||
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
|
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP', '/ConfigurationRepositoryP')
|
||||||
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
|
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
|
||||||
|
|
||||||
function Test-ArgKeyMatch {
|
function Test-ArgKeyMatch {
|
||||||
@@ -475,6 +595,16 @@ if (-not (Test-Path $ConfigDir)) {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Перечислены файлы — загрузка частичная. Иначе список молча игнорировался бы, а умолчание Full
|
||||||
|
# заменило бы всю конфигурацию базы.
|
||||||
|
if ($Files -or $ListFile) {
|
||||||
|
if ($Mode -eq "Full") {
|
||||||
|
Write-Host "[note] перечислены файлы — загружаются только они; -Mode Full не применён" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
$Mode = "Partial"
|
||||||
|
}
|
||||||
|
if (-not $Mode) { $Mode = "Full" }
|
||||||
|
|
||||||
# --- Validate Partial mode ---
|
# --- Validate Partial mode ---
|
||||||
if ($Mode -eq "Partial" -and -not $Files -and -not $ListFile) {
|
if ($Mode -eq "Partial" -and -not $Files -and -not $ListFile) {
|
||||||
Write-Host "Error: -Files or -ListFile required for Partial mode" -ForegroundColor Red
|
Write-Host "Error: -Files or -ListFile required for Partial mode" -ForegroundColor Red
|
||||||
@@ -494,7 +624,7 @@ try {
|
|||||||
}
|
}
|
||||||
if ($AllExtensions) {
|
if ($AllExtensions) {
|
||||||
$arguments = @("infobase", "config", "import", "all-extensions", "$ConfigDir", "--db-path=$InfoBasePath")
|
$arguments = @("infobase", "config", "import", "all-extensions", "$ConfigDir", "--db-path=$InfoBasePath")
|
||||||
} elseif ($Mode -eq "Partial" -or $Files -or $ListFile) {
|
} elseif ($Mode -eq "Partial") {
|
||||||
# partial: import specific files (relative to ConfigDir)
|
# partial: import specific files (relative to ConfigDir)
|
||||||
$fileList = @()
|
$fileList = @()
|
||||||
if ($ListFile) {
|
if ($ListFile) {
|
||||||
@@ -567,6 +697,11 @@ try {
|
|||||||
if ($UserName) { $arguments += "/N`"$UserName`"" }
|
if ($UserName) { $arguments += "/N`"$UserName`"" }
|
||||||
if ($Password) { $arguments += "/P`"$Password`"" }
|
if ($Password) { $arguments += "/P`"$Password`"" }
|
||||||
|
|
||||||
|
# База под хранилищем не примет НИ ОДНОЙ операции конфигуратора без этих реквизитов, а для
|
||||||
|
# базы вне хранилища они безвредны — поэтому подставляем всегда, когда они известны.
|
||||||
|
$__repo = Resolve-RepositorySettings
|
||||||
|
$arguments += Get-RepositoryArgs $__repo
|
||||||
|
|
||||||
$arguments += "/LoadConfigFromFiles", "`"$ConfigDir`""
|
$arguments += "/LoadConfigFromFiles", "`"$ConfigDir`""
|
||||||
|
|
||||||
if ($Mode -eq "Full") {
|
if ($Mode -eq "Full") {
|
||||||
@@ -631,7 +766,7 @@ try {
|
|||||||
$arguments += $extraArgs
|
$arguments += $extraArgs
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName, $__repo.Password))"
|
||||||
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
|
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
|
||||||
$exitCode = $__v8.ExitCode
|
$exitCode = $__v8.ExitCode
|
||||||
|
|
||||||
@@ -660,6 +795,7 @@ try {
|
|||||||
Write-Host "--- End ---"
|
Write-Host "--- End ---"
|
||||||
}
|
}
|
||||||
Write-PlatformOutput $__v8.Output
|
Write-PlatformOutput $__v8.Output
|
||||||
|
Write-RepositoryHints $logContent
|
||||||
|
|
||||||
# Причину не называем: строки лога печатаются следом и говорят за себя, а класс проблемы
|
# Причину не называем: строки лога печатаются следом и говорят за себя, а класс проблемы
|
||||||
# разный — от отброшенного свойства до нерабочей на этой платформе конфигурации. Подсказку
|
# разный — от отброшенного свойства до нерабочей на этой платформе конфигурации. Подсказку
|
||||||
+170
-31
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-load-xml v1.21 — Load 1C configuration from XML files
|
# db-load-xml v1.28 — Load 1C configuration from XML files
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -72,10 +72,116 @@ IBCMD_OWNED_KEYS = [
|
|||||||
"--import", "--export", "--apply", "--force", "--create-database",
|
"--import", "--export", "--apply", "--force", "--create-database",
|
||||||
"--user", "--password",
|
"--user", "--password",
|
||||||
]
|
]
|
||||||
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
|
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP", "/ConfigurationRepositoryP"]
|
||||||
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
|
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
|
||||||
|
|
||||||
|
|
||||||
|
# --- Реквизиты хранилища из .v8-project.json ---
|
||||||
|
# Модель их не передаёт: скрипт сопоставляет параметры соединения с записью в databases[]
|
||||||
|
# и берёт repository оттуда. Тот же приём, что в cf-edit.py (сопоставление по configSrc).
|
||||||
|
def _sg_find_v8project(start_dir):
|
||||||
|
d = start_dir
|
||||||
|
for _ in range(20):
|
||||||
|
if not d:
|
||||||
|
break
|
||||||
|
pj = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pj):
|
||||||
|
return pj
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
break
|
||||||
|
d = parent
|
||||||
|
return None
|
||||||
|
|
||||||
|
def same_path(a, b):
|
||||||
|
if not a or not b:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
return os.path.abspath(a).rstrip("\\/").lower() == os.path.abspath(b).rstrip("\\/").lower()
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def find_project_database(args):
|
||||||
|
"""Запись базы в реестре, соответствующая переданному соединению. None, если не найдена."""
|
||||||
|
pf = _sg_find_v8project(os.getcwd())
|
||||||
|
if not pf:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
proj = json.load(f)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
for db in proj.get("databases") or []:
|
||||||
|
if args.InfoBasePath and db.get("path") and same_path(db["path"], args.InfoBasePath):
|
||||||
|
return db
|
||||||
|
if args.InfoBaseServer and args.InfoBaseRef and db.get("server") and db.get("ref"):
|
||||||
|
if (db["server"].lower() == args.InfoBaseServer.lower()
|
||||||
|
and db["ref"].lower() == args.InfoBaseRef.lower()):
|
||||||
|
return db
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_repository_settings(args):
|
||||||
|
"""Возвращает dict path/user/password/from_registry. Явные -Repository* сильнее реестра."""
|
||||||
|
db_rec = find_project_database(args)
|
||||||
|
rec = None
|
||||||
|
if db_rec:
|
||||||
|
if args.Extension:
|
||||||
|
# У расширения СВОЁ хранилище со своим путём (проверено): выбирается парой
|
||||||
|
# /ConfigurationRepositoryF"<путь расширения>" + -Extension "<Имя>".
|
||||||
|
for ext in db_rec.get("extensions") or []:
|
||||||
|
if (ext.get("name") or "").lower() == args.Extension.lower():
|
||||||
|
rec = ext.get("repository")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
rec = db_rec.get("repository")
|
||||||
|
path = args.RepositoryPath or ((rec or {}).get("path") or None)
|
||||||
|
user = args.RepositoryUser or ((rec or {}).get("user") or None)
|
||||||
|
# Пустой пароль = отсутствующий: 1С требует опускать ключ целиком, а не передавать пустое значение.
|
||||||
|
pwd = args.RepositoryPassword or ((rec or {}).get("password") or None)
|
||||||
|
return {
|
||||||
|
"path": path.strip().strip('"') if path else None,
|
||||||
|
"user": user,
|
||||||
|
"password": pwd,
|
||||||
|
"from_registry": bool(rec and rec.get("path")),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def repository_args(repo):
|
||||||
|
"""Ключи доступа к хранилищу. Форма — кавычки ВНУТРИ токена, как у /N и /P."""
|
||||||
|
a = []
|
||||||
|
if not repo or not repo.get("path"):
|
||||||
|
return a
|
||||||
|
a.append('/ConfigurationRepositoryF"%s"' % repo["path"])
|
||||||
|
if repo.get("user"):
|
||||||
|
a.append('/ConfigurationRepositoryN"%s"' % repo["user"])
|
||||||
|
if repo.get("password"):
|
||||||
|
a.append('/ConfigurationRepositoryP"%s"' % repo["password"])
|
||||||
|
return a
|
||||||
|
|
||||||
|
|
||||||
|
# Сообщения платформы про хранилище конфигурации называют причину, но не действие. Действие
|
||||||
|
# дописываем сами: без него модель упирается в отказ и не знает, чем его лечить.
|
||||||
|
def write_repository_hints(log_text):
|
||||||
|
if not log_text:
|
||||||
|
return
|
||||||
|
if "текущая конфигурация помещена в хранилище" in log_text:
|
||||||
|
print("[hint] полная загрузка в базу, подключённую к хранилищу, невозможна.")
|
||||||
|
print(" Используйте -Mode Partial, предварительно захватив объекты: /db-repo lock")
|
||||||
|
for m in re.finditer(r"объект метаданных (\S+) не захвачен в хранилище", log_text):
|
||||||
|
obj = m.group(1)
|
||||||
|
if obj == "Configuration":
|
||||||
|
print("[hint] не захвачен корень конфигурации — он нужен, чтобы добавить или удалить объект:")
|
||||||
|
print(' /db-repo lock <база> -Objects "Конфигурация"')
|
||||||
|
else:
|
||||||
|
print('[hint] объект не захвачен в хранилище: /db-repo lock <база> -Objects "%s"' % obj)
|
||||||
|
if "Соединение с хранилищем конфигурации не установлено" in log_text:
|
||||||
|
print("[hint] соединение с хранилищем не установлено. Две причины:")
|
||||||
|
print(' реквизиты неизвестны — добавьте "repository" в запись базы в .v8-project.json (см. /db-list);')
|
||||||
|
print(" либо хранилище недоступно — для сетевого проверьте сервер хранилища и порт.")
|
||||||
|
|
||||||
|
|
||||||
def arg_key_match(token, key):
|
def arg_key_match(token, key):
|
||||||
"""Token matches a key when it equals it, or starts with it and the next character
|
"""Token matches a key when it equals it, or starts with it and the next character
|
||||||
is not a letter — catches glued /N"user" and --password=x, while keeping
|
is not a letter — catches glued /N"user" and --password=x, while keeping
|
||||||
@@ -120,7 +226,6 @@ def assert_extra_args(extra, engine, hints):
|
|||||||
print(
|
print(
|
||||||
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||||
f"({param} cannot extend the ibcmd command)",
|
f"({param} cannot extend the ibcmd command)",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
for k in owned:
|
for k in owned:
|
||||||
@@ -128,7 +233,6 @@ def assert_extra_args(extra, engine, hints):
|
|||||||
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||||
print(
|
print(
|
||||||
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@@ -196,14 +300,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
|||||||
print(
|
print(
|
||||||
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||||
"(use -AdditionalIbcmdArguments)",
|
"(use -AdditionalIbcmdArguments)",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if engine != "ibcmd" and ibcmd_extra:
|
if engine != "ibcmd" and ibcmd_extra:
|
||||||
print(
|
print(
|
||||||
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||||
"(use -AdditionalV8Arguments)",
|
"(use -AdditionalV8Arguments)",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if engine == "ibcmd":
|
if engine == "ibcmd":
|
||||||
@@ -245,14 +347,14 @@ def resolve_v8path(v8path):
|
|||||||
v8path = max(candidates, key=_version_key)
|
v8path = max(candidates, key=_version_key)
|
||||||
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
else:
|
else:
|
||||||
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
print("Error: 1C executable not found. Specify -V8Path")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
if os.path.isdir(v8path):
|
||||||
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
v8path = os.path.join(v8path, exe)
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
@@ -283,7 +385,7 @@ def assert_infobase_exists(path):
|
|||||||
if not path:
|
if not path:
|
||||||
return
|
return
|
||||||
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
|
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
|
||||||
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
|
print(f"Error: information base not found at {path} (no 1Cv8.1CD)")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
@@ -300,7 +402,7 @@ def clean_path(value, param=""):
|
|||||||
if len(v) > 3 and v[-1] in "\\/":
|
if len(v) > 3 and v[-1] in "\\/":
|
||||||
v = v[:-1]
|
v = v[:-1]
|
||||||
if '"' in v:
|
if '"' in v:
|
||||||
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
print(f"Error: {param or 'path'} contains a quote character: {value}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v
|
return v
|
||||||
|
|
||||||
@@ -319,11 +421,31 @@ def run_v8(v8path, arguments):
|
|||||||
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
|
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
|
||||||
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
|
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
|
||||||
escape those quotes, so there the command line is handed over ready-made.
|
escape those quotes, so there the command line is handed over ready-made.
|
||||||
|
|
||||||
|
На POSIX аргументы уходят СПИСКОМ, и кавычки, нужные для склейки на Windows, стали бы
|
||||||
|
частью значения: путь с пробелом платформа не находит («Неопределена информационная
|
||||||
|
база»), многословный -comment теряет молча. Поэтому здесь снимается ОДИН слой
|
||||||
|
обрамляющих кавычек. Склеенные ключи (/N"user", /ConfigurationRepositoryF"путь",
|
||||||
|
File="…") не задеты: у них кавычки внутри токена, а не по краям.
|
||||||
"""
|
"""
|
||||||
if os.name == "nt":
|
if os.name == "nt":
|
||||||
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||||
else:
|
else:
|
||||||
cmd = [v8path] + arguments
|
def strip_framing_quotes(a):
|
||||||
|
# Кавычки, которыми мы обрамляем значения ради склейки на Windows, на POSIX
|
||||||
|
# становятся ЧАСТЬЮ значения. Проверено на darwin: путь с пробелом отдельным
|
||||||
|
# токеном даёт «Неопределена информационная база», а склеенный
|
||||||
|
# /ConfigurationRepositoryF"путь с пробелом" — «завершилось с ошибкой»;
|
||||||
|
# без кавычек обе формы работают.
|
||||||
|
if len(a) > 1 and a[0] == '"' and a[-1] == '"':
|
||||||
|
return a[1:-1] # "значение" отдельным токеном
|
||||||
|
if a[0:1] == "/" and a[-1:] == '"' and '"' in a[:-1]:
|
||||||
|
i = a.index('"')
|
||||||
|
return a[:i] + a[i + 1:-1] # /N"имя" -> /Nимя
|
||||||
|
return a # File="…" не трогаем: там кавычки —
|
||||||
|
# часть синтаксиса строки соединения,
|
||||||
|
# и с ними на POSIX всё работает
|
||||||
|
cmd = [v8path] + [strip_framing_quotes(a) for a in arguments]
|
||||||
r = subprocess.run(cmd, input=b"", capture_output=True)
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
r.stdout = decode_platform_bytes(r.stdout)
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
r.stderr = decode_platform_bytes(r.stderr)
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
@@ -384,7 +506,7 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
|||||||
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
"""
|
"""
|
||||||
if warn_no_user and os.name == "nt" and not has_username:
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
sys.stderr.write(IBCMD_NOUSER_HINT)
|
sys.stdout.write(IBCMD_NOUSER_HINT)
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
r = subprocess.run(cmd, input=b"", capture_output=True)
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
r.stdout = decode_platform_bytes(r.stdout)
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
@@ -438,11 +560,14 @@ def main():
|
|||||||
parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server")
|
parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server")
|
||||||
parser.add_argument("-UserName", default="", help="1C user name")
|
parser.add_argument("-UserName", default="", help="1C user name")
|
||||||
parser.add_argument("-Password", default="", help="1C user password")
|
parser.add_argument("-Password", default="", help="1C user password")
|
||||||
|
parser.add_argument("-RepositoryPath", default="")
|
||||||
|
parser.add_argument("-RepositoryUser", default="")
|
||||||
|
parser.add_argument("-RepositoryPassword", default="")
|
||||||
parser.add_argument("-ConfigDir", required=True, help="Directory with XML configuration sources")
|
parser.add_argument("-ConfigDir", required=True, help="Directory with XML configuration sources")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-Mode",
|
"-Mode",
|
||||||
default="Full",
|
default="",
|
||||||
choices=["Full", "Partial"],
|
choices=["", "Full", "Partial"],
|
||||||
help="Load mode (default: Full)",
|
help="Load mode (default: Full)",
|
||||||
)
|
)
|
||||||
parser.add_argument("-Files", default="", help="Comma-separated relative file paths (for Partial mode)")
|
parser.add_argument("-Files", default="", help="Comma-separated relative file paths (for Partial mode)")
|
||||||
@@ -495,34 +620,42 @@ def main():
|
|||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if engine == "ibcmd":
|
if engine == "ibcmd":
|
||||||
if not args.InfoBasePath:
|
if not args.InfoBasePath:
|
||||||
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath)")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# --- Validate config dir ---
|
# --- Validate config dir ---
|
||||||
if not os.path.exists(args.ConfigDir):
|
if not os.path.exists(args.ConfigDir):
|
||||||
print(f"Error: config directory not found: {args.ConfigDir}", file=sys.stderr)
|
print(f"Error: config directory not found: {args.ConfigDir}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# --- Validate Partial mode ---
|
# --- Validate Partial mode ---
|
||||||
|
# Перечислены файлы — загрузка частичная. Иначе список молча игнорировался бы, а умолчание
|
||||||
|
# Full заменило бы всю конфигурацию базы.
|
||||||
|
if args.Files or args.ListFile:
|
||||||
|
if args.Mode == "Full":
|
||||||
|
print("[note] перечислены файлы — загружаются только они; -Mode Full не применён")
|
||||||
|
args.Mode = "Partial"
|
||||||
|
if not args.Mode:
|
||||||
|
args.Mode = "Full"
|
||||||
if args.Mode == "Partial" and not args.Files and not args.ListFile:
|
if args.Mode == "Partial" and not args.Files and not args.ListFile:
|
||||||
print("Error: -Files or -ListFile required for Partial mode", file=sys.stderr)
|
print("Error: -Files or -ListFile required for Partial mode")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# --- ibcmd branch (file infobase only; hierarchical full-directory import) ---
|
# --- ibcmd branch (file infobase only; hierarchical full-directory import) ---
|
||||||
if engine == "ibcmd":
|
if engine == "ibcmd":
|
||||||
if args.Format == "Plain":
|
if args.Format == "Plain":
|
||||||
print("Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)", file=sys.stderr)
|
print("Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if args.AllExtensions:
|
if args.AllExtensions:
|
||||||
arguments = ["infobase", "config", "import", "all-extensions", args.ConfigDir, f"--db-path={args.InfoBasePath}"]
|
arguments = ["infobase", "config", "import", "all-extensions", args.ConfigDir, f"--db-path={args.InfoBasePath}"]
|
||||||
elif args.Mode == "Partial" or args.Files or args.ListFile:
|
elif args.Mode == "Partial":
|
||||||
# partial: import specific files (relative to ConfigDir)
|
# partial: import specific files (relative to ConfigDir)
|
||||||
if args.ListFile:
|
if args.ListFile:
|
||||||
if not os.path.isfile(args.ListFile):
|
if not os.path.isfile(args.ListFile):
|
||||||
print(f"Error: list file not found: {args.ListFile}", file=sys.stderr)
|
print(f"Error: list file not found: {args.ListFile}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
with open(args.ListFile, encoding="utf-8-sig") as f:
|
with open(args.ListFile, encoding="utf-8-sig") as f:
|
||||||
file_list = [ln.strip() for ln in f if ln.strip()]
|
file_list = [ln.strip() for ln in f if ln.strip()]
|
||||||
@@ -531,7 +664,7 @@ def main():
|
|||||||
else:
|
else:
|
||||||
file_list = []
|
file_list = []
|
||||||
if not file_list:
|
if not file_list:
|
||||||
print("Error: -Files or -ListFile required for partial import", file=sys.stderr)
|
print("Error: -Files or -ListFile required for partial import")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
arguments = ["infobase", "config", "import", "files"] + file_list
|
arguments = ["infobase", "config", "import", "files"] + file_list
|
||||||
arguments += [f"--base-dir={args.ConfigDir}", f"--db-path={args.InfoBasePath}"]
|
arguments += [f"--base-dir={args.ConfigDir}", f"--db-path={args.InfoBasePath}"]
|
||||||
@@ -553,7 +686,7 @@ def main():
|
|||||||
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||||
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
print(f"Error loading configuration from files (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
|
print(f"Error loading configuration from files (code: {result.returncode}){describe_exit(result.returncode)}")
|
||||||
sys.exit(result.returncode)
|
sys.exit(result.returncode)
|
||||||
print(f"Configuration loaded successfully from: {args.ConfigDir}")
|
print(f"Configuration loaded successfully from: {args.ConfigDir}")
|
||||||
exit_code = 0
|
exit_code = 0
|
||||||
@@ -571,7 +704,7 @@ def main():
|
|||||||
if exit_code == 0:
|
if exit_code == 0:
|
||||||
print("Database configuration updated successfully")
|
print("Database configuration updated successfully")
|
||||||
else:
|
else:
|
||||||
print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
|
print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}")
|
||||||
print_platform_output(ar)
|
print_platform_output(ar)
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
@@ -593,6 +726,11 @@ def main():
|
|||||||
if args.Password:
|
if args.Password:
|
||||||
arguments.append(f'/P"{args.Password}"')
|
arguments.append(f'/P"{args.Password}"')
|
||||||
|
|
||||||
|
# База под хранилищем не примет НИ ОДНОЙ операции конфигуратора без этих реквизитов, а для
|
||||||
|
# базы вне хранилища они безвредны — поэтому подставляем всегда, когда они известны.
|
||||||
|
repo = resolve_repository_settings(args)
|
||||||
|
arguments.extend(repository_args(repo))
|
||||||
|
|
||||||
arguments += ["/LoadConfigFromFiles", f'"{args.ConfigDir}"']
|
arguments += ["/LoadConfigFromFiles", f'"{args.ConfigDir}"']
|
||||||
|
|
||||||
if args.Mode == "Full":
|
if args.Mode == "Full":
|
||||||
@@ -603,7 +741,7 @@ def main():
|
|||||||
# Build list file
|
# Build list file
|
||||||
if args.ListFile:
|
if args.ListFile:
|
||||||
if not os.path.isfile(args.ListFile):
|
if not os.path.isfile(args.ListFile):
|
||||||
print(f"Error: list file not found: {args.ListFile}", file=sys.stderr)
|
print(f"Error: list file not found: {args.ListFile}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
with open(args.ListFile, encoding="utf-8-sig") as f:
|
with open(args.ListFile, encoding="utf-8-sig") as f:
|
||||||
raw_list = [ln.strip() for ln in f if ln.strip()]
|
raw_list = [ln.strip() for ln in f if ln.strip()]
|
||||||
@@ -615,12 +753,12 @@ def main():
|
|||||||
support_files = [x for x in raw_list if support_re.search(x)]
|
support_files = [x for x in raw_list if support_re.search(x)]
|
||||||
file_list = [x for x in raw_list if not support_re.search(x)]
|
file_list = [x for x in raw_list if not support_re.search(x)]
|
||||||
if support_files:
|
if support_files:
|
||||||
print("[ВНИМАНИЕ] Служебные файлы состояния поддержки исключены из частичной загрузки (частично не грузятся):", file=sys.stderr)
|
print("[ВНИМАНИЕ] Служебные файлы состояния поддержки исключены из частичной загрузки (частично не грузятся):")
|
||||||
for sf in support_files:
|
for sf in support_files:
|
||||||
print(f" - {sf}", file=sys.stderr)
|
print(f" - {sf}")
|
||||||
print(" Смена состояния поддержки применяется только полной загрузкой: -Mode Full.", file=sys.stderr)
|
print(" Смена состояния поддержки применяется только полной загрузкой: -Mode Full.")
|
||||||
if not file_list:
|
if not file_list:
|
||||||
print("Error: после исключения служебных файлов поддержки загружать нечего. Для смены поддержки используйте -Mode Full.", file=sys.stderr)
|
print("Error: после исключения служебных файлов поддержки загружать нечего. Для смены поддержки используйте -Mode Full.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
generated_list_file = os.path.join(temp_dir, "load_list.txt")
|
generated_list_file = os.path.join(temp_dir, "load_list.txt")
|
||||||
with open(generated_list_file, "w", encoding="utf-8-sig") as f:
|
with open(generated_list_file, "w", encoding="utf-8-sig") as f:
|
||||||
@@ -652,7 +790,7 @@ def main():
|
|||||||
arguments.extend(quote_if_needed(a) for a in extra_args)
|
arguments.extend(quote_if_needed(a) for a in extra_args)
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName, repo['password'])}")
|
||||||
result = run_v8(v8path, arguments)
|
result = run_v8(v8path, arguments)
|
||||||
exit_code = result.returncode
|
exit_code = result.returncode
|
||||||
|
|
||||||
@@ -677,7 +815,7 @@ def main():
|
|||||||
if exit_code == 0:
|
if exit_code == 0:
|
||||||
print("Load completed successfully")
|
print("Load completed successfully")
|
||||||
else:
|
else:
|
||||||
print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
|
print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}")
|
||||||
|
|
||||||
if log_content:
|
if log_content:
|
||||||
print("--- Log ---")
|
print("--- Log ---")
|
||||||
@@ -685,6 +823,7 @@ def main():
|
|||||||
print("--- End ---")
|
print("--- End ---")
|
||||||
|
|
||||||
print_platform_output(result)
|
print_platform_output(result)
|
||||||
|
write_repository_hints(log_content)
|
||||||
# Причину не называем: строки лога печатаются следом и говорят за себя, а класс проблемы
|
# Причину не называем: строки лога печатаются следом и говорят за себя, а класс проблемы
|
||||||
# разный — от отброшенного свойства до нерабочей на этой платформе конфигурации. Подсказку
|
# разный — от отброшенного свойства до нерабочей на этой платформе конфигурации. Подсказку
|
||||||
# про -StrictLog не даём: загрузка уже выполнена, повторять её ради того же текста незачем.
|
# про -StrictLog не даём: загрузка уже выполнена, повторять её ради того же текста незачем.
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
---
|
||||||
|
name: db-repo
|
||||||
|
description: Работа с хранилищем конфигурации 1С. Используй когда нужно захватить объекты, поместить изменения в хранилище конфигурации, получить изменения из него, подключить базу к хранилищу
|
||||||
|
argument-hint: <lock|unlock|commit|update> [database] -Objects "<объекты>"
|
||||||
|
allowed-tools:
|
||||||
|
- Bash
|
||||||
|
- Read
|
||||||
|
- Glob
|
||||||
|
- AskUserQuestion
|
||||||
|
---
|
||||||
|
|
||||||
|
# /db-repo — Хранилище конфигурации 1С
|
||||||
|
|
||||||
|
Захват и помещение объектов, получение изменений, подключение базы, история версий,
|
||||||
|
администрирование хранилища.
|
||||||
|
|
||||||
|
> Хранилище конфигурации 1С, а не Git-репозиторий.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
/db-repo lock [database] -Objects "Справочник.Номенклатура"
|
||||||
|
/db-repo commit [database] -Objects "Справочник.Номенклатура" -Comment "Добавлен Артикул"
|
||||||
|
/db-repo unlock [database] -Objects "Справочник.Номенклатура"
|
||||||
|
/db-repo update [database]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Порядок работы
|
||||||
|
|
||||||
|
В базу, подключённую к хранилищу, исходники грузятся **только частично** и **только по захваченным**
|
||||||
|
объектам. Выполняй строго по шагам:
|
||||||
|
|
||||||
|
```
|
||||||
|
0. /db-repo update <база> — начать с актуального состояния
|
||||||
|
1. /db-repo lock <база> -Objects "Справочник.Номенклатура"
|
||||||
|
2. если шаг 0 или 1 напечатал «локальная конфигурация изменена, получено объектов из хранилища: N» —
|
||||||
|
выгрузи названные объекты: /db-dump-xml <база> <каталог> -Mode Partial -ObjectsFile "<файл из вывода>"
|
||||||
|
3. правки в исходниках: /meta-edit, /form-edit, /skd-edit, /meta-compile и т. д.
|
||||||
|
4. /db-load-xml <каталог> <база> -Mode Partial -Files "Catalogs/Номенклатура.xml,…" -UpdateDB
|
||||||
|
5. /db-repo commit <база> -Objects "Справочник.Номенклатура" -Comment "…"
|
||||||
|
```
|
||||||
|
|
||||||
|
Шаг 0 стоит делать всегда, когда работа не продолжается сразу после предыдущего цикла: правки
|
||||||
|
должны опираться на актуальное состояние — в том числе тех объектов, которые ты не меняешь, но
|
||||||
|
используешь.
|
||||||
|
|
||||||
|
Шаг 2 пропускать нельзя: захват и обновление подтягивают из хранилища свежие версии, и загрузка
|
||||||
|
исходников, снятых раньше, откатит чужие изменения — молча, без ошибки.
|
||||||
|
|
||||||
|
**Что вообще захватывается.** Отдельные объекты хранилища — сам объект, а также его **формы,
|
||||||
|
макеты и команды**. Реквизиты, табличные части, измерения и ресурсы отдельными объектами **не
|
||||||
|
являются**: они правятся в составе владельца.
|
||||||
|
|
||||||
|
| Что правишь | Что захватывать |
|
||||||
|
|-------------|-----------------|
|
||||||
|
| Реквизит, табличную часть, измерение, ресурс, модуль объекта | сам объект: `Справочник.Контрагенты` |
|
||||||
|
| Существующую форму, макет, команду | её саму: `Справочник.Контрагенты.Форма.ФормаЭлемента` |
|
||||||
|
| Добавляешь новую форму, макет, команду | объект-владельца; при помещении назови и новый объект |
|
||||||
|
| Добавляешь новый объект конфигурации | только корень: `Конфигурация`. Самого объекта ещё нет — захватить его нельзя; при помещении назови и его |
|
||||||
|
|
||||||
|
Захватывай минимум того, что правишь: чем шире захват, тем больше конфликтов с коллегами.
|
||||||
|
Захват объекта его формы и макеты не захватывает — для этого есть `-WithChildren`.
|
||||||
|
|
||||||
|
## Параметры подключения
|
||||||
|
|
||||||
|
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу:
|
||||||
|
1. Если пользователь указал параметры подключения — используй напрямую
|
||||||
|
2. Если указал базу по имени — ищи по id / alias / name
|
||||||
|
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||||
|
4. Если ветка не совпала — используй `default`
|
||||||
|
|
||||||
|
Реквизиты хранилища передавать не нужно: запись базы находится по переданным параметрам
|
||||||
|
соединения (`-InfoBasePath` либо `-InfoBaseServer` + `-InfoBaseRef`), реквизиты берутся из её
|
||||||
|
`repository`. Задать их явно можно параметрами `-Repository*`.
|
||||||
|
|
||||||
|
## Команда
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python ".gemini/skills/db-repo/scripts/db-repo.py" -Command <подкоманда> <параметры>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Рабочий цикл
|
||||||
|
|
||||||
|
| Подкоманда | Что делает |
|
||||||
|
|------------|------------|
|
||||||
|
| `lock` | Захватить объекты |
|
||||||
|
| `unlock` | Отменить захват |
|
||||||
|
| `commit` | Поместить изменения в хранилище |
|
||||||
|
| `update` | Получить изменения из хранилища |
|
||||||
|
|
||||||
|
### Параметры
|
||||||
|
|
||||||
|
| Параметр | Обязательный | Описание |
|
||||||
|
|----------|:------------:|----------|
|
||||||
|
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||||
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С |
|
||||||
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
|
| `-UserName <имя>` | нет | Пользователь базы |
|
||||||
|
| `-Password <пароль>` | нет | Пароль пользователя базы |
|
||||||
|
| `-Objects <список>` | усл. | Объекты через запятую. Для `lock`, `unlock`, `commit` обязателен, если не задан `-All` |
|
||||||
|
| `-ObjectsFile <путь>` | нет | Файл со списком объектов, одно имя на строку |
|
||||||
|
| `-All` | нет | Операция над всей конфигурацией — вместо `-Objects`, а не вместе с ним |
|
||||||
|
| `-WithChildren` | нет | Вместе с подчинёнными объектами на полную глубину |
|
||||||
|
| `-Comment <текст>` | нет | Комментарий к помещению (`commit`). Многострочный — как есть, с переводами строк |
|
||||||
|
| `-KeepLocked` | нет | Оставить объекты захваченными после помещения |
|
||||||
|
| `-Revised` | нет | Получать захваченные объекты, если потребуется |
|
||||||
|
| `-Force` | нет | Разное по подкомандам — см. ниже |
|
||||||
|
| `-Extension <имя>` | нет | Работать с хранилищем расширения |
|
||||||
|
| `-RepositoryPath <путь>` | нет | Хранилище явно, вместо реестра |
|
||||||
|
| `-RepositoryUser <имя>` | нет | Пользователь хранилища явно |
|
||||||
|
| `-RepositoryPassword <пароль>` | нет | Пароль пользователя хранилища явно |
|
||||||
|
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы `1cv8.exe` через запятую |
|
||||||
|
|
||||||
|
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||||
|
|
||||||
|
### `-Force`
|
||||||
|
|
||||||
|
| Подкоманда | Что делает |
|
||||||
|
|------------|------------|
|
||||||
|
| `unlock` | **Теряет локальные правки**: объекты перезаписываются версией из хранилища |
|
||||||
|
| `commit` | Пытается очистить ссылки на удалённые объекты вместо ошибки |
|
||||||
|
| `update` | Подтверждает добавление и удаление объектов конфигурации |
|
||||||
|
|
||||||
|
### Имена объектов
|
||||||
|
|
||||||
|
Объект — `Справочник.Номенклатура`. Форма, макет, команда — полным путём:
|
||||||
|
`Документ.ЗаказПокупателя.Форма.ФормаДокумента`, `Справочник.Номенклатура.Макет.Печать`.
|
||||||
|
Корень конфигурации — `Конфигурация`.
|
||||||
|
|
||||||
|
Если объект «не найден», это не всегда опечатка: он мог появиться в хранилище позже, чем
|
||||||
|
обновлялась база (`/db-repo update`), либо это вовсе не объект хранилища — реквизит или
|
||||||
|
табличная часть.
|
||||||
|
|
||||||
|
## Результат
|
||||||
|
|
||||||
|
Нулевой код не означает, что что-то изменилось. Под нулём приходят «уже захвачено», «обновлять
|
||||||
|
нечего», «помещать нечего» и частичный захват — когда часть объектов занята другими, а остальное
|
||||||
|
захвачено и его можно править.
|
||||||
|
|
||||||
|
**Читай текст вывода, а не только код.** Там же приходит список полученных из хранилища объектов,
|
||||||
|
который требует перевыгрузки перед правкой.
|
||||||
|
|
||||||
|
## Требуют подтверждения пользователя
|
||||||
|
|
||||||
|
Перед этими операциями **спроси подтверждение**:
|
||||||
|
|
||||||
|
| Операция | Почему |
|
||||||
|
|----------|--------|
|
||||||
|
| `lock -All` | Захватывает **всю конфигурацию**: на большой базе идёт долго и блокирует работу всей команде |
|
||||||
|
| `unlock -Force` | Теряются локальные правки захваченных объектов |
|
||||||
|
| `disconnect` | Теряется подключение базы к хранилищу, в том числе на стороне хранилища |
|
||||||
|
| `connect -ForceReplaceCfg` | Конфигурация базы заменяется конфигурацией из хранилища |
|
||||||
|
|
||||||
|
`update` не выполнится, если у базы в реестре не объявлено `repository`, а реквизиты не заданы
|
||||||
|
явно: на неподключённой к хранилищу базе эта команда заменяет всю конфигурацию его содержимым и
|
||||||
|
рапортует успех.
|
||||||
|
|
||||||
|
## Расширения
|
||||||
|
|
||||||
|
У расширения своё хранилище со своим путём. Укажи `-Extension "<Имя>"` — реквизиты возьмутся из
|
||||||
|
`extensions[].repository` записи базы. Подкоманды работают одинаково для основной конфигурации и
|
||||||
|
для расширения.
|
||||||
|
|
||||||
|
## Остальные задачи
|
||||||
|
|
||||||
|
| Файл | Про что |
|
||||||
|
|------|---------|
|
||||||
|
| [connect.md](references/connect.md) | Подключение и отключение базы от хранилища |
|
||||||
|
| [history.md](references/history.md) | История версий, отчёт, выгрузка версии в CF |
|
||||||
|
| [admin.md](references/admin.md) | Создание хранилища, пользователи и права |
|
||||||
|
| [service.md](references/service.md) | Метки версий, оптимизация, очистка кеша |
|
||||||
|
|
||||||
|
## Примеры
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Захватить справочник вместе с подчинёнными объектами
|
||||||
|
python ".gemini/skills/db-repo/scripts/db-repo.py" -Command lock -InfoBasePath "C:\Bases\MyDB" -Objects "Справочник.Номенклатура" -WithChildren
|
||||||
|
|
||||||
|
# Захватить корень — он нужен, чтобы добавить или удалить объект конфигурации
|
||||||
|
python ".gemini/skills/db-repo/scripts/db-repo.py" -Command lock -InfoBasePath "C:\Bases\MyDB" -Objects "Конфигурация"
|
||||||
|
|
||||||
|
# Поместить новый объект: он уже существует, поэтому называется вместе с корнем
|
||||||
|
python ".gemini/skills/db-repo/scripts/db-repo.py" -Command commit -InfoBasePath "C:\Bases\MyDB" -Objects "Конфигурация,Справочник.Склады" -Comment "Добавлен справочник Склады"
|
||||||
|
|
||||||
|
# Поместить с комментарием, оставив захват
|
||||||
|
python ".gemini/skills/db-repo/scripts/db-repo.py" -Command commit -InfoBasePath "C:\Bases\MyDB" -Objects "Справочник.Номенклатура" -Comment "Добавлен реквизит Артикул" -KeepLocked
|
||||||
|
|
||||||
|
# Получить изменения из хранилища
|
||||||
|
python ".gemini/skills/db-repo/scripts/db-repo.py" -Command update -InfoBasePath "C:\Bases\MyDB"
|
||||||
|
|
||||||
|
# Серверная база, расширение
|
||||||
|
python ".gemini/skills/db-repo/scripts/db-repo.py" -Command lock -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Extension "МоёРасширение" -Objects "Справочник.Номенклатура"
|
||||||
|
```
|
||||||
|
|
||||||
|
## После выполнения
|
||||||
|
|
||||||
|
- `lock` или `update` сообщил о полученных объектах — выполни `/db-dump-xml -Mode Partial` с
|
||||||
|
указанным в выводе файлом, и только потом правь исходники
|
||||||
|
- после `lock` правки идут через `/db-load-xml -Mode Partial` и `/db-update`
|
||||||
|
- изменения готовы — предложи `/db-repo commit` с комментарием
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Администрирование хранилища
|
||||||
|
|
||||||
|
## create — создать хранилище
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command create -InfoBasePath "C:\Bases\MyDB" -RepositoryPath "C:\Repo\MyApp" -RepositoryUser "Admin" -RepositoryPassword "…"
|
||||||
|
```
|
||||||
|
|
||||||
|
| Параметр | Описание |
|
||||||
|
|----------|----------|
|
||||||
|
| `-NoBind` | Не подключать базу к созданному хранилищу |
|
||||||
|
| `-AllowConfigurationChanges` | Включить возможность изменения, если конфигурация на поддержке без неё |
|
||||||
|
| `-ChangesAllowedRule <правило>` | Правило для объектов, изменения которых разрешены поставщиком |
|
||||||
|
| `-ChangesNotRecommendedRule <правило>` | То же для «изменения не рекомендуются» |
|
||||||
|
|
||||||
|
Правила: `ObjectNotEditable`, `ObjectIsEditableSupportEnabled`, `ObjectNotSupported`.
|
||||||
|
|
||||||
|
Без `-NoBind` база сразу подключается к созданному хранилищу. Создание — это версия 1.
|
||||||
|
|
||||||
|
Для расширения: `-Extension "<Имя>"` и отдельный путь — у расширения своё хранилище.
|
||||||
|
|
||||||
|
## add-user — создать пользователя
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command add-user -InfoBasePath "C:\Bases\MyDB" -NewUser "Ivanov" -NewUserPassword "…" -Rights LockObjects
|
||||||
|
```
|
||||||
|
|
||||||
|
| Право | Что даёт |
|
||||||
|
|-------|----------|
|
||||||
|
| `ReadOnly` | Просмотр |
|
||||||
|
| `LockObjects` | Захват объектов |
|
||||||
|
| `ManageConfigurationVersions` | Изменение состава версий |
|
||||||
|
| `Administration` | Административные функции |
|
||||||
|
|
||||||
|
`-RestoreDeletedUser` — восстановить одноимённого удалённого. Если пользователь с таким именем
|
||||||
|
существует, он **не** будет добавлен. Выполняющий должен иметь административные права.
|
||||||
|
|
||||||
|
## copy-users — скопировать пользователей из другого хранилища
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command copy-users -InfoBasePath "C:\Bases\MyDB" -SourcePath "\\srv01\repo\Other" -SourceUser "Admin" -SourcePassword "…"
|
||||||
|
```
|
||||||
|
|
||||||
|
`-SourcePath`, `-SourceUser`, `-SourcePassword` описывают хранилище-**источник**. Удалённые пользователи
|
||||||
|
не копируются; существующие не перезаписываются.
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# Подключение базы к хранилищу
|
||||||
|
|
||||||
|
## connect — подключить
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command connect -InfoBasePath "C:\Bases\MyDB" -RepositoryPath "\\srv01\repo\MyApp" -RepositoryUser "Ivanov" -RepositoryPassword "…"
|
||||||
|
```
|
||||||
|
|
||||||
|
| Параметр | Описание |
|
||||||
|
|----------|----------|
|
||||||
|
| `-ForceReplaceCfg` | Конфигурация базы непустая — подтвердить замену её конфигурацией из хранилища. **Спроси подтверждение у пользователя** |
|
||||||
|
| `-ForceBindAlreadyBindedUser` | Подключить, даже если у этого пользователя уже есть конфигурация, связанная с хранилищем |
|
||||||
|
|
||||||
|
На пустой базе `-ForceReplaceCfg` не нужен.
|
||||||
|
|
||||||
|
**Переподключение** базы, которая уже была подключена, требует обоих флагов: конфигурация в базе
|
||||||
|
не пустая (`-ForceReplaceCfg`), а за пользователем хранилища всё ещё числится эта база
|
||||||
|
(`-ForceBindAlreadyBindedUser`).
|
||||||
|
|
||||||
|
После подключения добавь `repository` в запись базы в `.v8-project.json` — иначе остальные
|
||||||
|
подкоманды придётся каждый раз звать с явными реквизитами, а `update` откажется работать.
|
||||||
|
|
||||||
|
## disconnect — отключить
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command disconnect -InfoBasePath "C:\Bases\MyDB"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Спроси подтверждение у пользователя.** Отключение снимает связь и на стороне самого хранилища:
|
||||||
|
запись о подключении удаляется. Подключить базу обратно можно, но это уже не рядовая операция —
|
||||||
|
понадобятся оба флага `connect` из раздела выше.
|
||||||
|
|
||||||
|
Если в базе есть захваченные и изменённые объекты, операция не выполнится. `-Force` выполняет её
|
||||||
|
всё равно, и эти изменения теряются.
|
||||||
|
|
||||||
|
## Расширения
|
||||||
|
|
||||||
|
У расширения своё хранилище: `-Extension "<Имя>"` указывай вместе с путём именно к нему, а не
|
||||||
|
к хранилищу основной конфигурации.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# История версий хранилища
|
||||||
|
|
||||||
|
## report — отчёт по версиям
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command report -InfoBasePath "C:\Bases\MyDB" -OutputFile "C:\tmp\repo.txt"
|
||||||
|
```
|
||||||
|
|
||||||
|
| Параметр | Описание |
|
||||||
|
|----------|----------|
|
||||||
|
| `-OutputFile <путь>` | Куда сохранить отчёт. Необязателен |
|
||||||
|
| `-NBegin <номер>` | С какой версии. `-1` — только последняя |
|
||||||
|
| `-NEnd <номер>` | По какую версию |
|
||||||
|
| `-DateBegin` / `-DateEnd` | Границы по датам |
|
||||||
|
| `-GroupByObject` | Группировать по объектам |
|
||||||
|
| `-GroupByComment` | Группировать по комментарию |
|
||||||
|
| `-ReportFormat <txt\|mxl>` | По умолчанию `txt` |
|
||||||
|
|
||||||
|
`txt` — с разделителем-табуляцией, разбирается построчно.
|
||||||
|
|
||||||
|
> На боевом хранилище полный отчёт строить не надо — тысячи версий. Нужна головная
|
||||||
|
> версия — `-NBegin -1`. Длинный отчёт в вывод не печатается: сузьте выборку
|
||||||
|
> параметрами ниже.
|
||||||
|
|
||||||
|
## dump-cfg — выгрузить версию в CF
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command dump-cfg -InfoBasePath "C:\Bases\MyDB" -OutputFile "C:\tmp\v120.cf" -Version 120
|
||||||
|
```
|
||||||
|
|
||||||
|
Без `-Version` (или при `-1`) выгружается последняя версия.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Сервисные операции
|
||||||
|
|
||||||
|
## set-label — метка на версию
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command set-label -InfoBasePath "C:\Bases\MyDB" -Label "Релиз 1.2" -Version 120 -Comment "Передано в тест"
|
||||||
|
```
|
||||||
|
|
||||||
|
Без `-Version` метка ставится на последнюю версию. Несуществующая версия — ошибка.
|
||||||
|
|
||||||
|
## optimize — оптимизация хранения
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command optimize -InfoBasePath "C:\Bases\MyDB"
|
||||||
|
```
|
||||||
|
|
||||||
|
Оптимизирует хранение данных в хранилище. Операция долгая.
|
||||||
|
|
||||||
|
## clear-cache — очистка кеша
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command clear-cache -InfoBasePath "C:\Bases\MyDB" -CacheScope local
|
||||||
|
```
|
||||||
|
|
||||||
|
| `-CacheScope` | Что чистит |
|
||||||
|
|---------------|------------|
|
||||||
|
| `local` (по умолчанию) | Локальный кеш версий конфигурации |
|
||||||
|
| `global` | Глобальный кеш версий |
|
||||||
|
| `db` | Локальную базу данных хранилища |
|
||||||
|
|
||||||
|
Пригождается, когда хранилище ведёт себя странно после сбоя сети или отката версии.
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -36,7 +36,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" <параметры>
|
python ".gemini/skills/db-run/scripts/db-run.py" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -64,14 +64,14 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" <пар
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Простой запуск
|
# Простой запуск
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin"
|
python ".gemini/skills/db-run/scripts/db-run.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin"
|
||||||
|
|
||||||
# Запуск с обработкой
|
# Запуск с обработкой
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Execute "C:\epf\МояОбработка.epf"
|
python ".gemini/skills/db-run/scripts/db-run.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Execute "C:\epf\МояОбработка.epf"
|
||||||
|
|
||||||
# Открыть по навигационной ссылке
|
# Открыть по навигационной ссылке
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -URL "e1cib/data/Справочник.Номенклатура"
|
python ".gemini/skills/db-run/scripts/db-run.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -URL "e1cib/data/Справочник.Номенклатура"
|
||||||
|
|
||||||
# Серверная база с параметром запуска
|
# Серверная база с параметром запуска
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -CParam "ЗапуститьОбновление"
|
python ".gemini/skills/db-run/scripts/db-run.py" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -CParam "ЗапуститьОбновление"
|
||||||
```
|
```
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# db-run v1.8 — Launch 1C:Enterprise
|
# db-run v1.10 — Launch 1C:Enterprise
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
.\db-run.ps1 -InfoBasePath "C:\Bases\MyDB" -CParam "ЗапуститьОбновление"
|
.\db-run.ps1 -InfoBasePath "C:\Bases\MyDB" -CParam "ЗапуститьОбновление"
|
||||||
#>
|
#>
|
||||||
|
|
||||||
[CmdletBinding()]
|
[CmdletBinding(PositionalBinding=$false)]
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[string]$V8Path,
|
[string]$V8Path,
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-run v1.8 — Launch 1C:Enterprise
|
# db-run v1.10 — Launch 1C:Enterprise
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -117,7 +117,6 @@ def assert_extra_args(extra, engine, hints):
|
|||||||
print(
|
print(
|
||||||
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||||
f"({param} cannot extend the ibcmd command)",
|
f"({param} cannot extend the ibcmd command)",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
for k in owned:
|
for k in owned:
|
||||||
@@ -125,7 +124,6 @@ def assert_extra_args(extra, engine, hints):
|
|||||||
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||||
print(
|
print(
|
||||||
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@@ -193,14 +191,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
|||||||
print(
|
print(
|
||||||
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||||
"(use -AdditionalIbcmdArguments)",
|
"(use -AdditionalIbcmdArguments)",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if engine != "ibcmd" and ibcmd_extra:
|
if engine != "ibcmd" and ibcmd_extra:
|
||||||
print(
|
print(
|
||||||
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||||
"(use -AdditionalV8Arguments)",
|
"(use -AdditionalV8Arguments)",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if engine == "ibcmd":
|
if engine == "ibcmd":
|
||||||
@@ -225,7 +221,7 @@ def clean_path(value, param=""):
|
|||||||
if len(v) > 3 and v[-1] in "\\/":
|
if len(v) > 3 and v[-1] in "\\/":
|
||||||
v = v[:-1]
|
v = v[:-1]
|
||||||
if '"' in v:
|
if '"' in v:
|
||||||
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
print(f"Error: {param or 'path'} contains a quote character: {value}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v
|
return v
|
||||||
|
|
||||||
@@ -260,14 +256,14 @@ def resolve_v8path(v8path):
|
|||||||
v8path = max(candidates, key=_version_key)
|
v8path = max(candidates, key=_version_key)
|
||||||
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
else:
|
else:
|
||||||
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
print("Error: 1C executable not found. Specify -V8Path")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
if os.path.isdir(v8path):
|
||||||
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
v8path = os.path.join(v8path, exe)
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
@@ -327,7 +323,7 @@ def main():
|
|||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
@@ -377,7 +373,7 @@ def main():
|
|||||||
time.sleep(0.2)
|
time.sleep(0.2)
|
||||||
rc = proc.poll()
|
rc = proc.poll()
|
||||||
if rc is not None:
|
if rc is not None:
|
||||||
print(f"Error: 1C:Enterprise exited immediately (code: {rc})", file=sys.stderr)
|
print(f"Error: 1C:Enterprise exited immediately (code: {rc})")
|
||||||
sys.exit(rc if rc and rc > 0 else 1)
|
sys.exit(rc if rc and rc > 0 else 1)
|
||||||
print(f"PID: {proc.pid}")
|
print(f"PID: {proc.pid}")
|
||||||
print("1C:Enterprise launched")
|
print("1C:Enterprise launched")
|
||||||
@@ -35,7 +35,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" <параметры>
|
python ".gemini/skills/db-update/scripts/db-update.py" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -78,11 +78,11 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" <п
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Обычное обновление (файловая база)
|
# Обычное обновление (файловая база)
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin"
|
python ".gemini/skills/db-update/scripts/db-update.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin"
|
||||||
|
|
||||||
# Динамическое обновление (серверная база)
|
# Динамическое обновление (серверная база)
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -Dynamic "+"
|
python ".gemini/skills/db-update/scripts/db-update.py" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -Dynamic "+"
|
||||||
|
|
||||||
# Обновление расширения
|
# Обновление расширения
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Extension "МоёРасширение"
|
python ".gemini/skills/db-update/scripts/db-update.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Extension "МоёРасширение"
|
||||||
```
|
```
|
||||||
+102
-4
@@ -1,4 +1,4 @@
|
|||||||
# db-update v1.15 — Update 1C database configuration
|
# db-update v1.19 — Update 1C database configuration
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
.\db-update.ps1 -InfoBasePath "C:\Bases\MyDB" -Dynamic "+" -Extension "МоёРасширение"
|
.\db-update.ps1 -InfoBasePath "C:\Bases\MyDB" -Dynamic "+" -Extension "МоёРасширение"
|
||||||
#>
|
#>
|
||||||
|
|
||||||
[CmdletBinding()]
|
[CmdletBinding(PositionalBinding=$false)]
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[string]$V8Path,
|
[string]$V8Path,
|
||||||
@@ -97,6 +97,15 @@ param(
|
|||||||
# но в логе есть отбраковка.
|
# но в логе есть отбраковка.
|
||||||
[switch]$StrictLog,
|
[switch]$StrictLog,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string]$RepositoryPath,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string]$RepositoryUser,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string]$RepositoryPassword,
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[string[]]$AdditionalV8Arguments = @(),
|
[string[]]$AdditionalV8Arguments = @(),
|
||||||
|
|
||||||
@@ -107,6 +116,90 @@ param(
|
|||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
# --- Реквизиты хранилища из .v8-project.json ---
|
||||||
|
# Модель их не передаёт: скрипт сопоставляет параметры соединения с записью в databases[]
|
||||||
|
# и берёт repository оттуда. Тот же приём, что в cf-edit.ps1 (сопоставление по configSrc).
|
||||||
|
function Find-V8Project([string]$startDir) {
|
||||||
|
$d = $startDir
|
||||||
|
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||||
|
$pj = Join-Path $d ".v8-project.json"
|
||||||
|
if (Test-Path $pj) { return $pj }
|
||||||
|
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||||
|
if ($parent -eq $d) { break }
|
||||||
|
$d = $parent
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
function Test-SamePath {
|
||||||
|
param([string]$A, [string]$B)
|
||||||
|
if (-not $A -or -not $B) { return $false }
|
||||||
|
try {
|
||||||
|
$na = [System.IO.Path]::GetFullPath($A).TrimEnd('\', '/')
|
||||||
|
$nb = [System.IO.Path]::GetFullPath($B).TrimEnd('\', '/')
|
||||||
|
return $na.Equals($nb, [System.StringComparison]::OrdinalIgnoreCase)
|
||||||
|
} catch { return $false }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Find-ProjectDatabase {
|
||||||
|
# Запись базы в реестре, соответствующая переданному соединению. $null, если не найдена.
|
||||||
|
$pf = Find-V8Project (Get-Location).Path
|
||||||
|
if (-not $pf) { return $null }
|
||||||
|
try { $proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json } catch { return $null }
|
||||||
|
if (-not $proj.databases) { return $null }
|
||||||
|
foreach ($db in $proj.databases) {
|
||||||
|
if ($InfoBasePath -and $db.path -and (Test-SamePath $db.path $InfoBasePath)) { return $db }
|
||||||
|
if ($InfoBaseServer -and $InfoBaseRef -and $db.server -and $db.ref) {
|
||||||
|
if ($db.server.Equals($InfoBaseServer, [System.StringComparison]::OrdinalIgnoreCase) -and
|
||||||
|
$db.ref.Equals($InfoBaseRef, [System.StringComparison]::OrdinalIgnoreCase)) { return $db }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-RepositorySettings {
|
||||||
|
# Возвращает @{ Path; User; Password; FromRegistry }. Явные -Repository* всегда сильнее реестра.
|
||||||
|
$dbRec = Find-ProjectDatabase
|
||||||
|
$rec = $null
|
||||||
|
if ($dbRec) {
|
||||||
|
if ($Extension) {
|
||||||
|
# У расширения СВОЁ хранилище со своим путём (проверено): выбирается парой
|
||||||
|
# /ConfigurationRepositoryF"<путь расширения>" + -Extension "<Имя>".
|
||||||
|
if ($dbRec.extensions) {
|
||||||
|
foreach ($ext in $dbRec.extensions) {
|
||||||
|
if ($ext.name -and $ext.name.Equals($Extension, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||||
|
$rec = $ext.repository
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$rec = $dbRec.repository
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$path = if ($RepositoryPath) { $RepositoryPath } elseif ($rec -and $rec.path) { [string]$rec.path } else { $null }
|
||||||
|
$user = if ($RepositoryUser) { $RepositoryUser } elseif ($rec -and $rec.user) { [string]$rec.user } else { $null }
|
||||||
|
# Пустой пароль = отсутствующий: 1С требует опускать ключ целиком, а не передавать пустое значение.
|
||||||
|
$pwd = if ($RepositoryPassword) { $RepositoryPassword } elseif ($rec -and $rec.password) { [string]$rec.password } else { $null }
|
||||||
|
return @{
|
||||||
|
Path = if ($path) { $path.Trim().Trim('"') } else { $null }
|
||||||
|
User = $user
|
||||||
|
Password = $pwd
|
||||||
|
FromRegistry = [bool]($rec -and $rec.path)
|
||||||
|
DbRecord = $dbRec
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-RepositoryArgs {
|
||||||
|
# Ключи доступа к хранилищу. Форма — кавычки ВНУТРИ токена, как у /N и /P.
|
||||||
|
param([hashtable]$Repo)
|
||||||
|
$a = @()
|
||||||
|
if (-not $Repo -or -not $Repo.Path) { return $a }
|
||||||
|
$a += "/ConfigurationRepositoryF`"$($Repo.Path)`""
|
||||||
|
if ($Repo.User) { $a += "/ConfigurationRepositoryN`"$($Repo.User)`"" }
|
||||||
|
if ($Repo.Password) { $a += "/ConfigurationRepositoryP`"$($Repo.Password)`"" }
|
||||||
|
return $a
|
||||||
|
}
|
||||||
|
|
||||||
function Protect-Secrets {
|
function Protect-Secrets {
|
||||||
# Redact literal secret values from a display string (String.Replace is literal, not regex).
|
# Redact literal secret values from a display string (String.Replace is literal, not regex).
|
||||||
param([string]$Text, [string[]]$Secrets)
|
param([string]$Text, [string[]]$Secrets)
|
||||||
@@ -145,7 +238,7 @@ $script:IbcmdOwnedKeys = @(
|
|||||||
'--import', '--export', '--apply', '--force', '--create-database',
|
'--import', '--export', '--apply', '--force', '--create-database',
|
||||||
'--user', '--password'
|
'--user', '--password'
|
||||||
)
|
)
|
||||||
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
|
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP', '/ConfigurationRepositoryP')
|
||||||
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
|
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
|
||||||
|
|
||||||
function Test-ArgKeyMatch {
|
function Test-ArgKeyMatch {
|
||||||
@@ -499,6 +592,11 @@ try {
|
|||||||
if ($UserName) { $arguments += "/N`"$UserName`"" }
|
if ($UserName) { $arguments += "/N`"$UserName`"" }
|
||||||
if ($Password) { $arguments += "/P`"$Password`"" }
|
if ($Password) { $arguments += "/P`"$Password`"" }
|
||||||
|
|
||||||
|
# База под хранилищем не примет НИ ОДНОЙ операции конфигуратора без этих реквизитов, а для
|
||||||
|
# базы вне хранилища они безвредны — поэтому подставляем всегда, когда они известны.
|
||||||
|
$__repo = Resolve-RepositorySettings
|
||||||
|
$arguments += Get-RepositoryArgs $__repo
|
||||||
|
|
||||||
$arguments += "/UpdateDBCfg"
|
$arguments += "/UpdateDBCfg"
|
||||||
|
|
||||||
# --- Options ---
|
# --- Options ---
|
||||||
@@ -526,7 +624,7 @@ try {
|
|||||||
$arguments += $extraArgs
|
$arguments += $extraArgs
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName, $__repo.Password))"
|
||||||
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
|
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
|
||||||
$exitCode = $__v8.ExitCode
|
$exitCode = $__v8.ExitCode
|
||||||
|
|
||||||
+127
-18
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-update v1.15 — Update 1C database configuration
|
# db-update v1.19 — Update 1C database configuration
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -72,10 +72,95 @@ IBCMD_OWNED_KEYS = [
|
|||||||
"--import", "--export", "--apply", "--force", "--create-database",
|
"--import", "--export", "--apply", "--force", "--create-database",
|
||||||
"--user", "--password",
|
"--user", "--password",
|
||||||
]
|
]
|
||||||
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
|
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP", "/ConfigurationRepositoryP"]
|
||||||
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
|
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
|
||||||
|
|
||||||
|
|
||||||
|
# --- Реквизиты хранилища из .v8-project.json ---
|
||||||
|
# Модель их не передаёт: скрипт сопоставляет параметры соединения с записью в databases[]
|
||||||
|
# и берёт repository оттуда. Тот же приём, что в cf-edit.py (сопоставление по configSrc).
|
||||||
|
def _sg_find_v8project(start_dir):
|
||||||
|
d = start_dir
|
||||||
|
for _ in range(20):
|
||||||
|
if not d:
|
||||||
|
break
|
||||||
|
pj = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pj):
|
||||||
|
return pj
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
break
|
||||||
|
d = parent
|
||||||
|
return None
|
||||||
|
|
||||||
|
def same_path(a, b):
|
||||||
|
if not a or not b:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
return os.path.abspath(a).rstrip("\\/").lower() == os.path.abspath(b).rstrip("\\/").lower()
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def find_project_database(args):
|
||||||
|
"""Запись базы в реестре, соответствующая переданному соединению. None, если не найдена."""
|
||||||
|
pf = _sg_find_v8project(os.getcwd())
|
||||||
|
if not pf:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
proj = json.load(f)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
for db in proj.get("databases") or []:
|
||||||
|
if args.InfoBasePath and db.get("path") and same_path(db["path"], args.InfoBasePath):
|
||||||
|
return db
|
||||||
|
if args.InfoBaseServer and args.InfoBaseRef and db.get("server") and db.get("ref"):
|
||||||
|
if (db["server"].lower() == args.InfoBaseServer.lower()
|
||||||
|
and db["ref"].lower() == args.InfoBaseRef.lower()):
|
||||||
|
return db
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_repository_settings(args):
|
||||||
|
"""Возвращает dict path/user/password/from_registry. Явные -Repository* сильнее реестра."""
|
||||||
|
db_rec = find_project_database(args)
|
||||||
|
rec = None
|
||||||
|
if db_rec:
|
||||||
|
if args.Extension:
|
||||||
|
# У расширения СВОЁ хранилище со своим путём (проверено): выбирается парой
|
||||||
|
# /ConfigurationRepositoryF"<путь расширения>" + -Extension "<Имя>".
|
||||||
|
for ext in db_rec.get("extensions") or []:
|
||||||
|
if (ext.get("name") or "").lower() == args.Extension.lower():
|
||||||
|
rec = ext.get("repository")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
rec = db_rec.get("repository")
|
||||||
|
path = args.RepositoryPath or ((rec or {}).get("path") or None)
|
||||||
|
user = args.RepositoryUser or ((rec or {}).get("user") or None)
|
||||||
|
# Пустой пароль = отсутствующий: 1С требует опускать ключ целиком, а не передавать пустое значение.
|
||||||
|
pwd = args.RepositoryPassword or ((rec or {}).get("password") or None)
|
||||||
|
return {
|
||||||
|
"path": path.strip().strip('"') if path else None,
|
||||||
|
"user": user,
|
||||||
|
"password": pwd,
|
||||||
|
"from_registry": bool(rec and rec.get("path")),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def repository_args(repo):
|
||||||
|
"""Ключи доступа к хранилищу. Форма — кавычки ВНУТРИ токена, как у /N и /P."""
|
||||||
|
a = []
|
||||||
|
if not repo or not repo.get("path"):
|
||||||
|
return a
|
||||||
|
a.append('/ConfigurationRepositoryF"%s"' % repo["path"])
|
||||||
|
if repo.get("user"):
|
||||||
|
a.append('/ConfigurationRepositoryN"%s"' % repo["user"])
|
||||||
|
if repo.get("password"):
|
||||||
|
a.append('/ConfigurationRepositoryP"%s"' % repo["password"])
|
||||||
|
return a
|
||||||
|
|
||||||
|
|
||||||
def arg_key_match(token, key):
|
def arg_key_match(token, key):
|
||||||
"""Token matches a key when it equals it, or starts with it and the next character
|
"""Token matches a key when it equals it, or starts with it and the next character
|
||||||
is not a letter — catches glued /N"user" and --password=x, while keeping
|
is not a letter — catches glued /N"user" and --password=x, while keeping
|
||||||
@@ -120,7 +205,6 @@ def assert_extra_args(extra, engine, hints):
|
|||||||
print(
|
print(
|
||||||
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||||
f"({param} cannot extend the ibcmd command)",
|
f"({param} cannot extend the ibcmd command)",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
for k in owned:
|
for k in owned:
|
||||||
@@ -128,7 +212,6 @@ def assert_extra_args(extra, engine, hints):
|
|||||||
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||||
print(
|
print(
|
||||||
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@@ -196,14 +279,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
|||||||
print(
|
print(
|
||||||
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||||
"(use -AdditionalIbcmdArguments)",
|
"(use -AdditionalIbcmdArguments)",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if engine != "ibcmd" and ibcmd_extra:
|
if engine != "ibcmd" and ibcmd_extra:
|
||||||
print(
|
print(
|
||||||
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||||
"(use -AdditionalV8Arguments)",
|
"(use -AdditionalV8Arguments)",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if engine == "ibcmd":
|
if engine == "ibcmd":
|
||||||
@@ -245,14 +326,14 @@ def resolve_v8path(v8path):
|
|||||||
v8path = max(candidates, key=_version_key)
|
v8path = max(candidates, key=_version_key)
|
||||||
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
else:
|
else:
|
||||||
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
print("Error: 1C executable not found. Specify -V8Path")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
if os.path.isdir(v8path):
|
||||||
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
v8path = os.path.join(v8path, exe)
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
@@ -283,7 +364,7 @@ def assert_infobase_exists(path):
|
|||||||
if not path:
|
if not path:
|
||||||
return
|
return
|
||||||
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
|
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
|
||||||
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
|
print(f"Error: information base not found at {path} (no 1Cv8.1CD)")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
@@ -300,7 +381,7 @@ def clean_path(value, param=""):
|
|||||||
if len(v) > 3 and v[-1] in "\\/":
|
if len(v) > 3 and v[-1] in "\\/":
|
||||||
v = v[:-1]
|
v = v[:-1]
|
||||||
if '"' in v:
|
if '"' in v:
|
||||||
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
print(f"Error: {param or 'path'} contains a quote character: {value}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v
|
return v
|
||||||
|
|
||||||
@@ -319,11 +400,31 @@ def run_v8(v8path, arguments):
|
|||||||
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
|
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
|
||||||
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
|
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
|
||||||
escape those quotes, so there the command line is handed over ready-made.
|
escape those quotes, so there the command line is handed over ready-made.
|
||||||
|
|
||||||
|
На POSIX аргументы уходят СПИСКОМ, и кавычки, нужные для склейки на Windows, стали бы
|
||||||
|
частью значения: путь с пробелом платформа не находит («Неопределена информационная
|
||||||
|
база»), многословный -comment теряет молча. Поэтому здесь снимается ОДИН слой
|
||||||
|
обрамляющих кавычек. Склеенные ключи (/N"user", /ConfigurationRepositoryF"путь",
|
||||||
|
File="…") не задеты: у них кавычки внутри токена, а не по краям.
|
||||||
"""
|
"""
|
||||||
if os.name == "nt":
|
if os.name == "nt":
|
||||||
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||||
else:
|
else:
|
||||||
cmd = [v8path] + arguments
|
def strip_framing_quotes(a):
|
||||||
|
# Кавычки, которыми мы обрамляем значения ради склейки на Windows, на POSIX
|
||||||
|
# становятся ЧАСТЬЮ значения. Проверено на darwin: путь с пробелом отдельным
|
||||||
|
# токеном даёт «Неопределена информационная база», а склеенный
|
||||||
|
# /ConfigurationRepositoryF"путь с пробелом" — «завершилось с ошибкой»;
|
||||||
|
# без кавычек обе формы работают.
|
||||||
|
if len(a) > 1 and a[0] == '"' and a[-1] == '"':
|
||||||
|
return a[1:-1] # "значение" отдельным токеном
|
||||||
|
if a[0:1] == "/" and a[-1:] == '"' and '"' in a[:-1]:
|
||||||
|
i = a.index('"')
|
||||||
|
return a[:i] + a[i + 1:-1] # /N"имя" -> /Nимя
|
||||||
|
return a # File="…" не трогаем: там кавычки —
|
||||||
|
# часть синтаксиса строки соединения,
|
||||||
|
# и с ними на POSIX всё работает
|
||||||
|
cmd = [v8path] + [strip_framing_quotes(a) for a in arguments]
|
||||||
r = subprocess.run(cmd, input=b"", capture_output=True)
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
r.stdout = decode_platform_bytes(r.stdout)
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
r.stderr = decode_platform_bytes(r.stderr)
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
@@ -384,7 +485,7 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
|||||||
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
"""
|
"""
|
||||||
if warn_no_user and os.name == "nt" and not has_username:
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
sys.stderr.write(IBCMD_NOUSER_HINT)
|
sys.stdout.write(IBCMD_NOUSER_HINT)
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
r = subprocess.run(cmd, input=b"", capture_output=True)
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
r.stdout = decode_platform_bytes(r.stdout)
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
@@ -438,6 +539,9 @@ def main():
|
|||||||
parser.add_argument("-InfoBaseRef", default="")
|
parser.add_argument("-InfoBaseRef", default="")
|
||||||
parser.add_argument("-UserName", default="")
|
parser.add_argument("-UserName", default="")
|
||||||
parser.add_argument("-Password", default="")
|
parser.add_argument("-Password", default="")
|
||||||
|
parser.add_argument("-RepositoryPath", default="")
|
||||||
|
parser.add_argument("-RepositoryUser", default="")
|
||||||
|
parser.add_argument("-RepositoryPassword", default="")
|
||||||
parser.add_argument("-Extension", default="")
|
parser.add_argument("-Extension", default="")
|
||||||
parser.add_argument("-AllExtensions", action="store_true")
|
parser.add_argument("-AllExtensions", action="store_true")
|
||||||
parser.add_argument("-Dynamic", default="", choices=["", "+", "-"])
|
parser.add_argument("-Dynamic", default="", choices=["", "+", "-"])
|
||||||
@@ -478,16 +582,16 @@ def main():
|
|||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if engine == "ibcmd":
|
if engine == "ibcmd":
|
||||||
if not args.InfoBasePath:
|
if not args.InfoBasePath:
|
||||||
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath)")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# --- ibcmd branch (file infobase only) ---
|
# --- ibcmd branch (file infobase only) ---
|
||||||
if engine == "ibcmd":
|
if engine == "ibcmd":
|
||||||
if args.AllExtensions:
|
if args.AllExtensions:
|
||||||
print("Error: ibcmd config apply does not support -AllExtensions (use -Extension)", file=sys.stderr)
|
print("Error: ibcmd config apply does not support -AllExtensions (use -Extension)")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
arguments = ["infobase", "config", "apply", f"--db-path={args.InfoBasePath}", "--force"]
|
arguments = ["infobase", "config", "apply", f"--db-path={args.InfoBasePath}", "--force"]
|
||||||
if args.Dynamic == "+":
|
if args.Dynamic == "+":
|
||||||
@@ -509,7 +613,7 @@ def main():
|
|||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
print("Database configuration updated successfully")
|
print("Database configuration updated successfully")
|
||||||
else:
|
else:
|
||||||
print(f"Error updating database configuration (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
|
print(f"Error updating database configuration (code: {result.returncode}){describe_exit(result.returncode)}")
|
||||||
sys.exit(result.returncode)
|
sys.exit(result.returncode)
|
||||||
|
|
||||||
# --- Temp dir ---
|
# --- Temp dir ---
|
||||||
@@ -530,6 +634,11 @@ def main():
|
|||||||
if args.Password:
|
if args.Password:
|
||||||
arguments.append(f'/P"{args.Password}"')
|
arguments.append(f'/P"{args.Password}"')
|
||||||
|
|
||||||
|
# База под хранилищем не примет НИ ОДНОЙ операции конфигуратора без этих реквизитов, а для
|
||||||
|
# базы вне хранилища они безвредны — поэтому подставляем всегда, когда они известны.
|
||||||
|
repo = resolve_repository_settings(args)
|
||||||
|
arguments.extend(repository_args(repo))
|
||||||
|
|
||||||
arguments.append("/UpdateDBCfg")
|
arguments.append("/UpdateDBCfg")
|
||||||
|
|
||||||
# --- Options ---
|
# --- Options ---
|
||||||
@@ -553,7 +662,7 @@ def main():
|
|||||||
arguments.extend(quote_if_needed(a) for a in extra_args)
|
arguments.extend(quote_if_needed(a) for a in extra_args)
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName, repo['password'])}")
|
||||||
result = run_v8(v8path, arguments)
|
result = run_v8(v8path, arguments)
|
||||||
exit_code = result.returncode
|
exit_code = result.returncode
|
||||||
|
|
||||||
@@ -561,7 +670,7 @@ def main():
|
|||||||
if exit_code == 0:
|
if exit_code == 0:
|
||||||
print("Database configuration updated successfully")
|
print("Database configuration updated successfully")
|
||||||
else:
|
else:
|
||||||
print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
|
print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}")
|
||||||
|
|
||||||
log_content = ""
|
log_content = ""
|
||||||
if os.path.isfile(out_file):
|
if os.path.isfile(out_file):
|
||||||
@@ -40,7 +40,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" <параметры>
|
python ".gemini/skills/epf-build/scripts/epf-build.py" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -64,8 +64,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" <п
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Сборка обработки (файловая база)
|
# Сборка обработки (файловая база)
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf"
|
python ".gemini/skills/epf-build/scripts/epf-build.py" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf"
|
||||||
|
|
||||||
# Серверная база
|
# Серверная база
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf"
|
python ".gemini/skills/epf-build/scripts/epf-build.py" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf"
|
||||||
```
|
```
|
||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
# epf-build v1.13 — Build external data processor or report (EPF/ERF) from XML sources
|
# epf-build v1.16 — Build external data processor or report (EPF/ERF) from XML sources
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
.\epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src\МойОтчёт.xml" -OutputFile "build\МойОтчёт.erf"
|
.\epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src\МойОтчёт.xml" -OutputFile "build\МойОтчёт.erf"
|
||||||
#>
|
#>
|
||||||
|
|
||||||
[CmdletBinding()]
|
[CmdletBinding(PositionalBinding=$false)]
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[string]$V8Path,
|
[string]$V8Path,
|
||||||
+34
-18
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# epf-build v1.13 — Build external data processor or report (EPF/ERF) from XML sources
|
# epf-build v1.16 — Build external data processor or report (EPF/ERF) from XML sources
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -120,7 +120,6 @@ def assert_extra_args(extra, engine, hints):
|
|||||||
print(
|
print(
|
||||||
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||||
f"({param} cannot extend the ibcmd command)",
|
f"({param} cannot extend the ibcmd command)",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
for k in owned:
|
for k in owned:
|
||||||
@@ -128,7 +127,6 @@ def assert_extra_args(extra, engine, hints):
|
|||||||
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||||
print(
|
print(
|
||||||
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@@ -196,14 +194,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
|||||||
print(
|
print(
|
||||||
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||||
"(use -AdditionalIbcmdArguments)",
|
"(use -AdditionalIbcmdArguments)",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if engine != "ibcmd" and ibcmd_extra:
|
if engine != "ibcmd" and ibcmd_extra:
|
||||||
print(
|
print(
|
||||||
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||||
"(use -AdditionalV8Arguments)",
|
"(use -AdditionalV8Arguments)",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if engine == "ibcmd":
|
if engine == "ibcmd":
|
||||||
@@ -245,14 +241,14 @@ def resolve_v8path(v8path):
|
|||||||
v8path = max(candidates, key=_version_key)
|
v8path = max(candidates, key=_version_key)
|
||||||
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
else:
|
else:
|
||||||
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
print("Error: 1C executable not found. Specify -V8Path")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
if os.path.isdir(v8path):
|
||||||
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
v8path = os.path.join(v8path, exe)
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
@@ -283,7 +279,7 @@ def assert_infobase_exists(path):
|
|||||||
if not path:
|
if not path:
|
||||||
return
|
return
|
||||||
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
|
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
|
||||||
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
|
print(f"Error: information base not found at {path} (no 1Cv8.1CD)")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
@@ -300,7 +296,7 @@ def clean_path(value, param=""):
|
|||||||
if len(v) > 3 and v[-1] in "\\/":
|
if len(v) > 3 and v[-1] in "\\/":
|
||||||
v = v[:-1]
|
v = v[:-1]
|
||||||
if '"' in v:
|
if '"' in v:
|
||||||
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
print(f"Error: {param or 'path'} contains a quote character: {value}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v
|
return v
|
||||||
|
|
||||||
@@ -319,11 +315,31 @@ def run_v8(v8path, arguments):
|
|||||||
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
|
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
|
||||||
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
|
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
|
||||||
escape those quotes, so there the command line is handed over ready-made.
|
escape those quotes, so there the command line is handed over ready-made.
|
||||||
|
|
||||||
|
На POSIX аргументы уходят СПИСКОМ, и кавычки, нужные для склейки на Windows, стали бы
|
||||||
|
частью значения: путь с пробелом платформа не находит («Неопределена информационная
|
||||||
|
база»), многословный -comment теряет молча. Поэтому здесь снимается ОДИН слой
|
||||||
|
обрамляющих кавычек. Склеенные ключи (/N"user", /ConfigurationRepositoryF"путь",
|
||||||
|
File="…") не задеты: у них кавычки внутри токена, а не по краям.
|
||||||
"""
|
"""
|
||||||
if os.name == "nt":
|
if os.name == "nt":
|
||||||
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||||
else:
|
else:
|
||||||
cmd = [v8path] + arguments
|
def strip_framing_quotes(a):
|
||||||
|
# Кавычки, которыми мы обрамляем значения ради склейки на Windows, на POSIX
|
||||||
|
# становятся ЧАСТЬЮ значения. Проверено на darwin: путь с пробелом отдельным
|
||||||
|
# токеном даёт «Неопределена информационная база», а склеенный
|
||||||
|
# /ConfigurationRepositoryF"путь с пробелом" — «завершилось с ошибкой»;
|
||||||
|
# без кавычек обе формы работают.
|
||||||
|
if len(a) > 1 and a[0] == '"' and a[-1] == '"':
|
||||||
|
return a[1:-1] # "значение" отдельным токеном
|
||||||
|
if a[0:1] == "/" and a[-1:] == '"' and '"' in a[:-1]:
|
||||||
|
i = a.index('"')
|
||||||
|
return a[:i] + a[i + 1:-1] # /N"имя" -> /Nимя
|
||||||
|
return a # File="…" не трогаем: там кавычки —
|
||||||
|
# часть синтаксиса строки соединения,
|
||||||
|
# и с ними на POSIX всё работает
|
||||||
|
cmd = [v8path] + [strip_framing_quotes(a) for a in arguments]
|
||||||
r = subprocess.run(cmd, input=b"", capture_output=True)
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
r.stdout = decode_platform_bytes(r.stdout)
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
r.stderr = decode_platform_bytes(r.stderr)
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
@@ -352,7 +368,7 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
|||||||
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
"""
|
"""
|
||||||
if warn_no_user and os.name == "nt" and not has_username:
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
sys.stderr.write(IBCMD_NOUSER_HINT)
|
sys.stdout.write(IBCMD_NOUSER_HINT)
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
r = subprocess.run(cmd, input=b"", capture_output=True)
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
r.stdout = decode_platform_bytes(r.stdout)
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
@@ -420,7 +436,7 @@ def main():
|
|||||||
}
|
}
|
||||||
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
|
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
|
||||||
if engine == "ibcmd" and args.InfoBaseServer and args.InfoBaseRef:
|
if engine == "ibcmd" and args.InfoBaseServer and args.InfoBaseRef:
|
||||||
print("Error: ibcmd supports file infobases only (use -InfoBasePath or omit for stub)", file=sys.stderr)
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath or omit for stub)")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# --- Auto-create stub database if no connection specified ---
|
# --- Auto-create stub database if no connection specified ---
|
||||||
@@ -441,14 +457,14 @@ def main():
|
|||||||
stub_cmd += ["-AdditionalIbcmdArguments"] + list(ibcmd_extra)
|
stub_cmd += ["-AdditionalIbcmdArguments"] + list(ibcmd_extra)
|
||||||
result = subprocess.run(stub_cmd, capture_output=False)
|
result = subprocess.run(stub_cmd, capture_output=False)
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
print("Error: failed to create stub database", file=sys.stderr)
|
print("Error: failed to create stub database")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
args.InfoBasePath = auto_base_path
|
args.InfoBasePath = auto_base_path
|
||||||
auto_created_base = auto_base_path
|
auto_created_base = auto_base_path
|
||||||
|
|
||||||
# --- Validate source file ---
|
# --- Validate source file ---
|
||||||
if not os.path.isfile(args.SourceFile):
|
if not os.path.isfile(args.SourceFile):
|
||||||
print(f"Error: source file not found: {args.SourceFile}", file=sys.stderr)
|
print(f"Error: source file not found: {args.SourceFile}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# --- Ensure output directory exists ---
|
# --- Ensure output directory exists ---
|
||||||
@@ -482,9 +498,9 @@ def main():
|
|||||||
if exit_code == 0:
|
if exit_code == 0:
|
||||||
print(f"External data processor/report built successfully: {args.OutputFile}")
|
print(f"External data processor/report built successfully: {args.OutputFile}")
|
||||||
elif out_missing:
|
elif out_missing:
|
||||||
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — build produced no output", file=sys.stderr)
|
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — build produced no output")
|
||||||
else:
|
else:
|
||||||
print(f"Error building external data processor/report (code: {exit_code})", file=sys.stderr)
|
print(f"Error building external data processor/report (code: {exit_code})")
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
@@ -521,9 +537,9 @@ def main():
|
|||||||
if exit_code == 0:
|
if exit_code == 0:
|
||||||
print(f"Build completed successfully: {args.OutputFile}")
|
print(f"Build completed successfully: {args.OutputFile}")
|
||||||
elif out_missing:
|
elif out_missing:
|
||||||
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — build produced no output", file=sys.stderr)
|
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — build produced no output")
|
||||||
else:
|
else:
|
||||||
print(f"Error building (code: {exit_code})", file=sys.stderr)
|
print(f"Error building (code: {exit_code})")
|
||||||
|
|
||||||
if os.path.isfile(out_file):
|
if os.path.isfile(out_file):
|
||||||
try:
|
try:
|
||||||
@@ -39,7 +39,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" <параметры>
|
python ".gemini/skills/epf-dump/scripts/epf-dump.py" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -64,8 +64,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" <па
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Разборка обработки (файловая база)
|
# Разборка обработки (файловая база)
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МояОбработка.epf" -OutputDir "src"
|
python ".gemini/skills/epf-dump/scripts/epf-dump.py" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МояОбработка.epf" -OutputDir "src"
|
||||||
|
|
||||||
# Серверная база
|
# Серверная база
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МояОбработка.epf" -OutputDir "src"
|
python ".gemini/skills/epf-dump/scripts/epf-dump.py" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МояОбработка.epf" -OutputDir "src"
|
||||||
```
|
```
|
||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
# epf-dump v1.12 — Dump external data processor or report (EPF/ERF) to XML sources
|
# epf-dump v1.15 — Dump external data processor or report (EPF/ERF) to XML sources
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
.\epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build\МойОтчёт.erf" -OutputDir "src"
|
.\epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build\МойОтчёт.erf" -OutputDir "src"
|
||||||
#>
|
#>
|
||||||
|
|
||||||
[CmdletBinding()]
|
[CmdletBinding(PositionalBinding=$false)]
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[string]$V8Path,
|
[string]$V8Path,
|
||||||
+35
-19
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# epf-dump v1.12 — Dump external data processor or report (EPF/ERF) to XML sources
|
# epf-dump v1.15 — Dump external data processor or report (EPF/ERF) to XML sources
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -120,7 +120,6 @@ def assert_extra_args(extra, engine, hints):
|
|||||||
print(
|
print(
|
||||||
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||||
f"({param} cannot extend the ibcmd command)",
|
f"({param} cannot extend the ibcmd command)",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
for k in owned:
|
for k in owned:
|
||||||
@@ -128,7 +127,6 @@ def assert_extra_args(extra, engine, hints):
|
|||||||
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||||
print(
|
print(
|
||||||
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@@ -196,14 +194,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
|||||||
print(
|
print(
|
||||||
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||||
"(use -AdditionalIbcmdArguments)",
|
"(use -AdditionalIbcmdArguments)",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if engine != "ibcmd" and ibcmd_extra:
|
if engine != "ibcmd" and ibcmd_extra:
|
||||||
print(
|
print(
|
||||||
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||||
"(use -AdditionalV8Arguments)",
|
"(use -AdditionalV8Arguments)",
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if engine == "ibcmd":
|
if engine == "ibcmd":
|
||||||
@@ -245,14 +241,14 @@ def resolve_v8path(v8path):
|
|||||||
v8path = max(candidates, key=_version_key)
|
v8path = max(candidates, key=_version_key)
|
||||||
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
else:
|
else:
|
||||||
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
print("Error: 1C executable not found. Specify -V8Path")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
if os.path.isdir(v8path):
|
||||||
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
v8path = os.path.join(v8path, exe)
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
@@ -283,7 +279,7 @@ def assert_infobase_exists(path):
|
|||||||
if not path:
|
if not path:
|
||||||
return
|
return
|
||||||
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
|
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
|
||||||
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
|
print(f"Error: information base not found at {path} (no 1Cv8.1CD)")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
@@ -300,7 +296,7 @@ def clean_path(value, param=""):
|
|||||||
if len(v) > 3 and v[-1] in "\\/":
|
if len(v) > 3 and v[-1] in "\\/":
|
||||||
v = v[:-1]
|
v = v[:-1]
|
||||||
if '"' in v:
|
if '"' in v:
|
||||||
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
print(f"Error: {param or 'path'} contains a quote character: {value}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v
|
return v
|
||||||
|
|
||||||
@@ -319,11 +315,31 @@ def run_v8(v8path, arguments):
|
|||||||
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
|
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
|
||||||
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
|
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
|
||||||
escape those quotes, so there the command line is handed over ready-made.
|
escape those quotes, so there the command line is handed over ready-made.
|
||||||
|
|
||||||
|
На POSIX аргументы уходят СПИСКОМ, и кавычки, нужные для склейки на Windows, стали бы
|
||||||
|
частью значения: путь с пробелом платформа не находит («Неопределена информационная
|
||||||
|
база»), многословный -comment теряет молча. Поэтому здесь снимается ОДИН слой
|
||||||
|
обрамляющих кавычек. Склеенные ключи (/N"user", /ConfigurationRepositoryF"путь",
|
||||||
|
File="…") не задеты: у них кавычки внутри токена, а не по краям.
|
||||||
"""
|
"""
|
||||||
if os.name == "nt":
|
if os.name == "nt":
|
||||||
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||||
else:
|
else:
|
||||||
cmd = [v8path] + arguments
|
def strip_framing_quotes(a):
|
||||||
|
# Кавычки, которыми мы обрамляем значения ради склейки на Windows, на POSIX
|
||||||
|
# становятся ЧАСТЬЮ значения. Проверено на darwin: путь с пробелом отдельным
|
||||||
|
# токеном даёт «Неопределена информационная база», а склеенный
|
||||||
|
# /ConfigurationRepositoryF"путь с пробелом" — «завершилось с ошибкой»;
|
||||||
|
# без кавычек обе формы работают.
|
||||||
|
if len(a) > 1 and a[0] == '"' and a[-1] == '"':
|
||||||
|
return a[1:-1] # "значение" отдельным токеном
|
||||||
|
if a[0:1] == "/" and a[-1:] == '"' and '"' in a[:-1]:
|
||||||
|
i = a.index('"')
|
||||||
|
return a[:i] + a[i + 1:-1] # /N"имя" -> /Nимя
|
||||||
|
return a # File="…" не трогаем: там кавычки —
|
||||||
|
# часть синтаксиса строки соединения,
|
||||||
|
# и с ними на POSIX всё работает
|
||||||
|
cmd = [v8path] + [strip_framing_quotes(a) for a in arguments]
|
||||||
r = subprocess.run(cmd, input=b"", capture_output=True)
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
r.stdout = decode_platform_bytes(r.stdout)
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
r.stderr = decode_platform_bytes(r.stderr)
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
@@ -352,7 +368,7 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
|||||||
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
"""
|
"""
|
||||||
if warn_no_user and os.name == "nt" and not has_username:
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
sys.stderr.write(IBCMD_NOUSER_HINT)
|
sys.stdout.write(IBCMD_NOUSER_HINT)
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
r = subprocess.run(cmd, input=b"", capture_output=True)
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
r.stdout = decode_platform_bytes(r.stdout)
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
@@ -428,20 +444,20 @@ def main():
|
|||||||
|
|
||||||
# --- Validate database connection ---
|
# --- Validate database connection ---
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
print("Error: database connection required. Specify -InfoBasePath or -InfoBaseServer/-InfoBaseRef", file=sys.stderr)
|
print("Error: database connection required. Specify -InfoBasePath or -InfoBaseServer/-InfoBaseRef")
|
||||||
print("Dump in an empty database loses reference types (CatalogRef, DocumentRef, etc.) irreversibly.")
|
print("Dump in an empty database loses reference types (CatalogRef, DocumentRef, etc.) irreversibly.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if engine == "ibcmd":
|
if engine == "ibcmd":
|
||||||
if not args.InfoBasePath:
|
if not args.InfoBasePath:
|
||||||
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath)")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if args.Format == "Plain":
|
if args.Format == "Plain":
|
||||||
print("Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)", file=sys.stderr)
|
print("Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# --- Validate input file ---
|
# --- Validate input file ---
|
||||||
if not os.path.isfile(args.InputFile):
|
if not os.path.isfile(args.InputFile):
|
||||||
print(f"Error: input file not found: {args.InputFile}", file=sys.stderr)
|
print(f"Error: input file not found: {args.InputFile}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# --- Ensure output directory exists ---
|
# --- Ensure output directory exists ---
|
||||||
@@ -473,9 +489,9 @@ def main():
|
|||||||
if exit_code == 0:
|
if exit_code == 0:
|
||||||
print(f"External data processor/report dumped successfully to: {args.OutputDir}")
|
print(f"External data processor/report dumped successfully to: {args.OutputDir}")
|
||||||
elif out_missing:
|
elif out_missing:
|
||||||
print(f"Error: exit code 0 but no files under {args.OutputDir} — dump produced no output", file=sys.stderr)
|
print(f"Error: exit code 0 but no files under {args.OutputDir} — dump produced no output")
|
||||||
else:
|
else:
|
||||||
print(f"Error dumping external data processor/report (code: {exit_code})", file=sys.stderr)
|
print(f"Error dumping external data processor/report (code: {exit_code})")
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
@@ -513,9 +529,9 @@ def main():
|
|||||||
if exit_code == 0:
|
if exit_code == 0:
|
||||||
print(f"Dump completed successfully to: {args.OutputDir}")
|
print(f"Dump completed successfully to: {args.OutputDir}")
|
||||||
elif out_missing:
|
elif out_missing:
|
||||||
print(f"Error: exit code 0 but no files under {args.OutputDir} — dump produced no output", file=sys.stderr)
|
print(f"Error: exit code 0 but no files under {args.OutputDir} — dump produced no output")
|
||||||
else:
|
else:
|
||||||
print(f"Error dumping (code: {exit_code})", file=sys.stderr)
|
print(f"Error dumping (code: {exit_code})")
|
||||||
|
|
||||||
if os.path.isfile(out_file):
|
if os.path.isfile(out_file):
|
||||||
try:
|
try:
|
||||||
@@ -37,7 +37,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-FormatVersion "<версия>"]
|
python ".gemini/skills/epf-init/scripts/init.py" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-FormatVersion "<версия>"]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Дальнейшие шаги
|
## Дальнейшие шаги
|
||||||
@@ -24,7 +24,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-validate.ps1" -ObjectPath "src/МояОбработка"
|
python ".gemini/skills/epf-validate/scripts/epf-validate.py" -ObjectPath "src/МояОбработка"
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-validate.ps1" -ObjectPath "src/МояОбработка/МояОбработка.xml"
|
python ".gemini/skills/epf-validate/scripts/epf-validate.py" -ObjectPath "src/МояОбработка/МояОбработка.xml"
|
||||||
```
|
```
|
||||||
|
|
||||||
+3
-2
@@ -1,8 +1,9 @@
|
|||||||
# epf-validate v1.5 — Validate 1C external data processor / report structure
|
# epf-validate v1.6 — Validate 1C external data processor / report structure
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects
|
# Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects
|
||||||
|
[CmdletBinding(PositionalBinding=$false)]
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory, Position=0)]
|
||||||
[Alias('Path')]
|
[Alias('Path')]
|
||||||
[string]$ObjectPath,
|
[string]$ObjectPath,
|
||||||
|
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# epf-validate v1.5 — Validate 1C external data processor / report structure
|
# epf-validate v1.6 — Validate 1C external data processor / report structure
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects
|
# Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ allowed-tools:
|
|||||||
Используй общий скрипт из epf-build:
|
Используй общий скрипт из epf-build:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.ps1" <параметры>
|
python ".gemini/skills/epf-build/scripts/epf-build.py" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -66,8 +66,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-bu
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Сборка отчёта (файловая база)
|
# Сборка отчёта (файловая база)
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.ps1" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf"
|
python ".gemini/skills/epf-build/scripts/epf-build.py" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf"
|
||||||
|
|
||||||
# Серверная база
|
# Серверная база
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf"
|
python ".gemini/skills/epf-build/scripts/epf-build.py" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf"
|
||||||
```
|
```
|
||||||
@@ -41,7 +41,7 @@ allowed-tools:
|
|||||||
Используй общий скрипт из epf-dump:
|
Используй общий скрипт из epf-dump:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.ps1" <параметры>
|
python ".gemini/skills/epf-dump/scripts/epf-dump.py" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -66,8 +66,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dum
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Разборка отчёта (файловая база)
|
# Разборка отчёта (файловая база)
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.ps1" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МойОтчёт.erf" -OutputDir "src"
|
python ".gemini/skills/epf-dump/scripts/epf-dump.py" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МойОтчёт.erf" -OutputDir "src"
|
||||||
|
|
||||||
# Серверная база
|
# Серверная база
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МойОтчёт.erf" -OutputDir "src"
|
python ".gemini/skills/epf-dump/scripts/epf-dump.py" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МойОтчёт.erf" -OutputDir "src"
|
||||||
```
|
```
|
||||||
@@ -38,7 +38,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-FormatVersion "<версия>"] [-WithSKD]
|
python ".gemini/skills/erf-init/scripts/init.py" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-FormatVersion "<версия>"] [-WithSKD]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Дальнейшие шаги
|
## Дальнейшие шаги
|
||||||
@@ -26,7 +26,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-validate/scripts/epf-validate.ps1" -ObjectPath "src/МойОтчёт"
|
python ".gemini/skills/epf-validate/scripts/epf-validate.py" -ObjectPath "src/МойОтчёт"
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-validate/scripts/epf-validate.ps1" -ObjectPath "src/МойОтчёт/МойОтчёт.xml"
|
python ".gemini/skills/epf-validate/scripts/epf-validate.py" -ObjectPath "src/МойОтчёт/МойОтчёт.xml"
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
---
|
||||||
|
name: form-add
|
||||||
|
description: Добавить пустую управляемую форму к объекту 1С. Используй когда нужно создать у объекта новую форму
|
||||||
|
argument-hint: <ObjectPath> <FormName> [-Purpose <Purpose>] [-Synonym <Synonym>] [-SetDefault]
|
||||||
|
allowed-tools:
|
||||||
|
- Bash
|
||||||
|
- Read
|
||||||
|
- Write
|
||||||
|
- Edit
|
||||||
|
- Glob
|
||||||
|
- Grep
|
||||||
|
---
|
||||||
|
|
||||||
|
# /form-add — Добавление формы к объекту конфигурации
|
||||||
|
|
||||||
|
Создаёт управляемую форму (metadata XML + Form.xml + Module.bsl) и регистрирует её в корневом XML объекта конфигурации (Document, Catalog, InformationRegister и др.).
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
/form-add <ObjectPath> <FormName> [-Purpose <Purpose>] [-Synonym <Synonym>] [-SetDefault]
|
||||||
|
```
|
||||||
|
|
||||||
|
| Параметр | Обязательный | По умолчанию | Описание |
|
||||||
|
|-------------|:------------:|--------------|----------------------------------------------|
|
||||||
|
| ObjectPath | да | — | Путь к XML-файлу объекта (Documents/Док.xml) |
|
||||||
|
| FormName | да | — | Имя формы (ФормаДокумента) |
|
||||||
|
| Purpose | нет | основная форма вида | Назначение формы — см. таблицу ниже: у справочника это форма объекта, у регистра сведений — форма записи, у журнала — форма списка |
|
||||||
|
| Synonym | нет | = FormName | Синоним формы |
|
||||||
|
| -SetDefault | нет | авто | Сделать основной. Без флага основной становится первая форма каждого назначения |
|
||||||
|
|
||||||
|
## Команда
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python ".gemini/skills/form-add/scripts/form-add.py" -ObjectPath "<ObjectPath>" -FormName "<FormName>" [-Purpose "<Purpose>"] [-Synonym "<Synonym>"] [-SetDefault]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Purpose — назначение формы
|
||||||
|
|
||||||
|
| Purpose | Какая форма | Становится основной |
|
||||||
|
|---------|-------------|---------------------|
|
||||||
|
| Object | форма объекта (элемента, документа, обработки) | да |
|
||||||
|
| List | форма списка | да |
|
||||||
|
| Choice | форма выбора | да |
|
||||||
|
| Folder | форма группы | да |
|
||||||
|
| FolderChoice | форма выбора группы | да |
|
||||||
|
| Record | форма записи | да |
|
||||||
|
| RecordSet | форма набора записей | нет — в платформе нет такого свойства |
|
||||||
|
| Save | форма сохранения настроек | да |
|
||||||
|
| Load | форма загрузки настроек | да |
|
||||||
|
| Custom | произвольная форма, без привязки к объекту | нет |
|
||||||
|
|
||||||
|
### Что доступно типу объекта
|
||||||
|
|
||||||
|
| Тип объекта | Назначения |
|
||||||
|
|-------------|------------|
|
||||||
|
| Catalog, ChartOfCharacteristicTypes | Object, Folder, List, Choice, FolderChoice, Custom |
|
||||||
|
| Document, ChartOfAccounts, ChartOfCalculationTypes, ExchangePlan, BusinessProcess, Task | Object, List, Choice, Custom |
|
||||||
|
| DataProcessor, Report, ExternalDataProcessor, ExternalReport | Object, Custom |
|
||||||
|
| InformationRegister | Record, List, RecordSet, Custom |
|
||||||
|
| AccumulationRegister, AccountingRegister, CalculationRegister | List, RecordSet, Custom |
|
||||||
|
| DocumentJournal, FilterCriterion | List, Custom |
|
||||||
|
| Enum | List, Choice, Custom |
|
||||||
|
| SettingsStorage | Save, Load, Custom |
|
||||||
|
|
||||||
|
Недопустимое сочетание отклоняется со списком доступных для этого типа. У константы собственных
|
||||||
|
форм нет — для неё используется общая форма (`CommonForm`).
|
||||||
|
|
||||||
|
## Примеры
|
||||||
|
|
||||||
|
```
|
||||||
|
# Форма документа
|
||||||
|
/form-add Documents/АвансовыйОтчет.xml ФормаДокумента -Purpose Object
|
||||||
|
|
||||||
|
# Форма списка каталога
|
||||||
|
/form-add Catalogs/Контрагенты.xml ФормаСписка -Purpose List
|
||||||
|
|
||||||
|
# Форма записи регистра сведений
|
||||||
|
/form-add InformationRegisters/КурсыВалют.xml ФормаЗаписи -Purpose Record
|
||||||
|
|
||||||
|
# Форма выбора с синонимом
|
||||||
|
/form-add Catalogs/Номенклатура.xml ФормаВыбора -Purpose Choice -Synonym "Выбор номенклатуры"
|
||||||
|
|
||||||
|
# Установить как форму по умолчанию
|
||||||
|
/form-add Documents/Заказ.xml ФормаДокументаНовая -Purpose Object -SetDefault
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. `/form-add` — создать каркас формы
|
||||||
|
2. `/form-compile` или `/form-edit` — наполнить Form.xml элементами
|
||||||
|
3. `/form-validate` — проверить корректность
|
||||||
|
4. `/form-info` — проанализировать результат
|
||||||
+254
-150
@@ -1,5 +1,6 @@
|
|||||||
# form-add v1.25 — Add managed form to 1C config object (+write_xml_file/write_utf8_bom: общий эталон записи)
|
# form-add v1.28 — Add managed form to 1C config object
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
[CmdletBinding(PositionalBinding=$false)]
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
[string]$ObjectPath,
|
[string]$ObjectPath,
|
||||||
@@ -9,8 +10,15 @@ param(
|
|||||||
|
|
||||||
[string]$Synonym = $FormName,
|
[string]$Synonym = $FormName,
|
||||||
|
|
||||||
[string]$Purpose = "Object",
|
# Пусто = основная форма вида (Primary в таблице): у справочника это форма объекта,
|
||||||
|
# у регистра сведений — форма записи, у журнала — форма списка. Жёсткое "Object"
|
||||||
|
# по умолчанию было бы неверным для видов, у которых формы объекта не бывает.
|
||||||
|
[string]$Purpose = "",
|
||||||
|
|
||||||
|
# Алиас с дефисом внутри имени: вызов вида --set-default PowerShell разбирает как имя
|
||||||
|
# параметра "set-default" и без алиаса отвечает отказом биндинга. Написания -SetDefault,
|
||||||
|
# --SetDefault и --setdefault совпадают с именем параметра и так.
|
||||||
|
[Alias('set-default')]
|
||||||
[switch]$SetDefault
|
[switch]$SetDefault
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -241,26 +249,166 @@ if (-not $metaDataObject) {
|
|||||||
$metaDataObject = $xmlDoc.DocumentElement
|
$metaDataObject = $xmlDoc.DocumentElement
|
||||||
}
|
}
|
||||||
|
|
||||||
$supportedTypes = @(
|
# --- Таблица видов: вид → допустимые назначения ---
|
||||||
"Document", "Catalog", "DataProcessor", "Report",
|
#
|
||||||
"ExternalDataProcessor", "ExternalReport",
|
# Одна запись на вид вместо разрозненных списков «поддерживаемые типы», «объектные типы»,
|
||||||
"InformationRegister", "AccumulationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
|
# «обработко-подобные» и «карта типов реквизита». Раньше они расходились молча: DocumentJournal
|
||||||
"ExchangePlan", "BusinessProcess", "Task", "DocumentJournal"
|
# был среди поддерживаемых, но не в карте типов, и в форму уходило `cfg:.Журнал` — платформа
|
||||||
)
|
# такую выгрузку не принимает, а навык рапортовал успех.
|
||||||
|
#
|
||||||
|
# MainAttr — тип главного реквизита; `{0}` подставляется именем объекта:
|
||||||
|
# "DynamicList" — динамический список (добавляется Settings/MainTable);
|
||||||
|
# $null — произвольная форма, блока Attributes нет вовсе.
|
||||||
|
# Slot — свойство объекта под «основную форму»; $null — такого свойства у вида нет.
|
||||||
|
# Эталон таблицы — docs/1c-form-spec.md, сверяется гардом check-form-purposes.mjs.
|
||||||
|
|
||||||
|
$formKinds = @{
|
||||||
|
"Catalog" = @{
|
||||||
|
"Object" = @{ MainAttr = "CatalogObject.{1}"; AttrName = "Объект"; Slot = "DefaultObjectForm"; SavedData = $true; Primary = $true }
|
||||||
|
"Folder" = @{ MainAttr = "CatalogObject.{1}"; AttrName = "Объект"; Slot = "DefaultFolderForm"; SavedData = $true }
|
||||||
|
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultListForm" }
|
||||||
|
"Choice" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultChoiceForm" }
|
||||||
|
"FolderChoice" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultFolderChoiceForm" }
|
||||||
|
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
|
||||||
|
}
|
||||||
|
"ChartOfCharacteristicTypes" = @{
|
||||||
|
"Object" = @{ MainAttr = "ChartOfCharacteristicTypesObject.{1}"; AttrName = "Объект"; Slot = "DefaultObjectForm"; SavedData = $true; Primary = $true }
|
||||||
|
"Folder" = @{ MainAttr = "ChartOfCharacteristicTypesObject.{1}"; AttrName = "Объект"; Slot = "DefaultFolderForm"; SavedData = $true }
|
||||||
|
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultListForm" }
|
||||||
|
"Choice" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultChoiceForm" }
|
||||||
|
"FolderChoice" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultFolderChoiceForm" }
|
||||||
|
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
|
||||||
|
}
|
||||||
|
"Document" = @{
|
||||||
|
"Object" = @{ MainAttr = "DocumentObject.{1}"; AttrName = "Объект"; Slot = "DefaultObjectForm"; SavedData = $true; Primary = $true }
|
||||||
|
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultListForm" }
|
||||||
|
"Choice" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultChoiceForm" }
|
||||||
|
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
|
||||||
|
}
|
||||||
|
"ChartOfAccounts" = @{
|
||||||
|
"Object" = @{ MainAttr = "ChartOfAccountsObject.{1}"; AttrName = "Объект"; Slot = "DefaultObjectForm"; SavedData = $true; Primary = $true }
|
||||||
|
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultListForm" }
|
||||||
|
"Choice" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultChoiceForm" }
|
||||||
|
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
|
||||||
|
}
|
||||||
|
"ChartOfCalculationTypes" = @{
|
||||||
|
"Object" = @{ MainAttr = "ChartOfCalculationTypesObject.{1}"; AttrName = "Объект"; Slot = "DefaultObjectForm"; SavedData = $true; Primary = $true }
|
||||||
|
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultListForm" }
|
||||||
|
"Choice" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultChoiceForm" }
|
||||||
|
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
|
||||||
|
}
|
||||||
|
"ExchangePlan" = @{
|
||||||
|
"Object" = @{ MainAttr = "ExchangePlanObject.{1}"; AttrName = "Объект"; Slot = "DefaultObjectForm"; SavedData = $true; Primary = $true }
|
||||||
|
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultListForm" }
|
||||||
|
"Choice" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultChoiceForm" }
|
||||||
|
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
|
||||||
|
}
|
||||||
|
"BusinessProcess" = @{
|
||||||
|
"Object" = @{ MainAttr = "BusinessProcessObject.{1}"; AttrName = "Объект"; Slot = "DefaultObjectForm"; SavedData = $true; Primary = $true }
|
||||||
|
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultListForm" }
|
||||||
|
"Choice" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultChoiceForm" }
|
||||||
|
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
|
||||||
|
}
|
||||||
|
"Task" = @{
|
||||||
|
"Object" = @{ MainAttr = "TaskObject.{1}"; AttrName = "Объект"; Slot = "DefaultObjectForm"; SavedData = $true; Primary = $true }
|
||||||
|
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultListForm" }
|
||||||
|
"Choice" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultChoiceForm" }
|
||||||
|
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
|
||||||
|
}
|
||||||
|
"DataProcessor" = @{
|
||||||
|
"Object" = @{ MainAttr = "DataProcessorObject.{1}"; AttrName = "Объект"; Slot = "DefaultForm"; Primary = $true }
|
||||||
|
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
|
||||||
|
}
|
||||||
|
"Report" = @{
|
||||||
|
"Object" = @{ MainAttr = "ReportObject.{1}"; AttrName = "Объект"; Slot = "DefaultForm"; Primary = $true }
|
||||||
|
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
|
||||||
|
}
|
||||||
|
"ExternalDataProcessor" = @{
|
||||||
|
"Object" = @{ MainAttr = "ExternalDataProcessorObject.{1}"; AttrName = "Объект"; Slot = "DefaultForm"; Primary = $true }
|
||||||
|
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
|
||||||
|
}
|
||||||
|
"ExternalReport" = @{
|
||||||
|
"Object" = @{ MainAttr = "ExternalReportObject.{1}"; AttrName = "Объект"; Slot = "DefaultForm"; Primary = $true }
|
||||||
|
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
|
||||||
|
}
|
||||||
|
"InformationRegister" = @{
|
||||||
|
"Record" = @{ MainAttr = "InformationRegisterRecordManager.{1}"; AttrName = "Запись"; Slot = "DefaultRecordForm"; SavedData = $true; Primary = $true }
|
||||||
|
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultListForm" }
|
||||||
|
"RecordSet" = @{ MainAttr = "InformationRegisterRecordSet.{1}"; AttrName = "Набор"; Slot = $null; SavedData = $true }
|
||||||
|
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
|
||||||
|
}
|
||||||
|
"AccumulationRegister" = @{
|
||||||
|
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultListForm"; Primary = $true }
|
||||||
|
"RecordSet" = @{ MainAttr = "AccumulationRegisterRecordSet.{1}"; AttrName = "Набор"; Slot = $null; SavedData = $true }
|
||||||
|
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
|
||||||
|
}
|
||||||
|
"AccountingRegister" = @{
|
||||||
|
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultListForm"; Primary = $true }
|
||||||
|
"RecordSet" = @{ MainAttr = "AccountingRegisterRecordSet.{1}"; AttrName = "Набор"; Slot = $null; SavedData = $true }
|
||||||
|
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
|
||||||
|
}
|
||||||
|
"CalculationRegister" = @{
|
||||||
|
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultListForm"; Primary = $true }
|
||||||
|
"RecordSet" = @{ MainAttr = "CalculationRegisterRecordSet.{1}"; AttrName = "Набор"; Slot = $null; SavedData = $true }
|
||||||
|
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
|
||||||
|
}
|
||||||
|
"DocumentJournal" = @{
|
||||||
|
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultForm"; Primary = $true }
|
||||||
|
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
|
||||||
|
}
|
||||||
|
"FilterCriterion" = @{
|
||||||
|
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultForm"; Primary = $true }
|
||||||
|
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
|
||||||
|
}
|
||||||
|
"Enum" = @{
|
||||||
|
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultListForm"; Primary = $true }
|
||||||
|
"Choice" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultChoiceForm" }
|
||||||
|
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
|
||||||
|
}
|
||||||
|
"SettingsStorage" = @{
|
||||||
|
"Save" = @{ MainAttr = $null; AttrName = $null; Slot = "DefaultSaveForm"; Primary = $true }
|
||||||
|
"Load" = @{ MainAttr = $null; AttrName = $null; Slot = "DefaultLoadForm" }
|
||||||
|
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Виды, у которых свойство DefaultForm есть, но собственных форм не бывает — отказ с причиной,
|
||||||
|
# а не «тип не поддерживается».
|
||||||
|
$noOwnForms = @{
|
||||||
|
"Constant" = "у константы нет собственных форм — используйте общую форму (CommonForm)"
|
||||||
|
}
|
||||||
|
|
||||||
|
$supportedTypes = @($formKinds.Keys) + @($noOwnForms.Keys)
|
||||||
|
|
||||||
|
# Отдельный факт, не выводимый из таблицы назначений: у форм обработок и отчётов в метаданных
|
||||||
|
# формы есть <ExtendedPresentation>.
|
||||||
|
$processorLikeTypes = @("DataProcessor", "Report", "ExternalDataProcessor", "ExternalReport")
|
||||||
|
|
||||||
|
# Вид объекта — первый элемент-потомок MetaDataObject, а не первое совпавшее по всему документу
|
||||||
|
# имя. Поиск по документу зависел от порядка перебора видов: у бизнес-процесса есть свойство
|
||||||
|
# <Task>, и он определялся как задача, после чего имя объекта не находилось вовсе.
|
||||||
$objectType = $null
|
$objectType = $null
|
||||||
$objectNode = $null
|
$objectNode = $null
|
||||||
foreach ($t in $supportedTypes) {
|
foreach ($child in $metaDataObject.ChildNodes) {
|
||||||
$node = $xmlDoc.SelectSingleNode("//md:$t", $nsMgr)
|
if ($child.NodeType -eq [System.Xml.XmlNodeType]::Element) {
|
||||||
if ($node) {
|
$objectType = $child.LocalName
|
||||||
$objectType = $t
|
$objectNode = $child
|
||||||
$objectNode = $node
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($objectType -and -not ($formKinds.ContainsKey($objectType) -or $noOwnForms.ContainsKey($objectType))) {
|
||||||
|
Write-Error "Тип объекта '$objectType' не поддерживается. Поддерживаемые типы: $(($formKinds.Keys | Sort-Object) -join ', ')"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
if (-not $objectType) {
|
if (-not $objectType) {
|
||||||
Write-Error "Не удалось определить тип объекта. Поддерживаемые типы: $($supportedTypes -join ', ')"
|
Write-Error "Не удалось определить тип объекта. Поддерживаемые типы: $(($formKinds.Keys | Sort-Object) -join ', ')"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($noOwnForms.ContainsKey($objectType)) {
|
||||||
|
Write-Error "$objectType не поддерживается: $($noOwnForms[$objectType])"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,44 +426,58 @@ Write-Host "Object: $objectType.$objectName"
|
|||||||
|
|
||||||
# --- Фаза 2: Валидация Purpose ---
|
# --- Фаза 2: Валидация Purpose ---
|
||||||
|
|
||||||
$Purpose = $Purpose.Substring(0,1).ToUpper() + $Purpose.Substring(1).ToLower()
|
# Назначение ищем в таблице регистронезависимо — как принимает PowerShell (в py-порту .lower()).
|
||||||
# Нормализация
|
$kindPurposes = $formKinds[$objectType]
|
||||||
switch ($Purpose) {
|
|
||||||
"Object" { }
|
# Обиходные написания назначения приводим к канону молча: русское название вида формы и
|
||||||
"List" { }
|
# английское с суффиксом Form. Ключ нормализуем — регистр, пробелы и разделители не значимы.
|
||||||
"Choice" { }
|
# Канон в документации один; здесь только приём ошибочного ввода, чтобы вызов не падал на форме
|
||||||
"Record" { }
|
# записи вместо назначения. Применимость назначения к виду объекта проверяется ниже как обычно.
|
||||||
default {
|
$purposeSynonyms = @{
|
||||||
Write-Error "Недопустимое назначение: $Purpose. Допустимые: Object, List, Choice, Record"
|
"формаобъекта"="Object"; "формаэлемента"="Object"; "формадокумента"="Object"
|
||||||
exit 1
|
"объект"="Object"; "элемент"="Object"; "документ"="Object"; "objectform"="Object"
|
||||||
|
"формасписка"="List"; "список"="List"; "listform"="List"
|
||||||
|
"формавыбора"="Choice"; "выбор"="Choice"; "choiceform"="Choice"
|
||||||
|
"формагруппы"="Folder"; "группа"="Folder"; "folderform"="Folder"
|
||||||
|
"формавыборагруппы"="FolderChoice"; "выборгруппы"="FolderChoice"; "folderchoiceform"="FolderChoice"
|
||||||
|
"формазаписи"="Record"; "запись"="Record"; "recordform"="Record"
|
||||||
|
"форманаборазаписей"="RecordSet"; "наборзаписей"="RecordSet"; "recordsetform"="RecordSet"
|
||||||
|
"формасохранения"="Save"; "формасохранениянастроек"="Save"; "сохранение"="Save"; "saveform"="Save"
|
||||||
|
"формазагрузки"="Load"; "формазагрузкинастроек"="Load"; "загрузка"="Load"; "loadform"="Load"
|
||||||
|
"произвольная"="Custom"; "произвольнаяформа"="Custom"; "customform"="Custom"
|
||||||
|
}
|
||||||
|
if ($Purpose) {
|
||||||
|
$purposeProbe = ($Purpose -replace '[\s_-]', '').ToLowerInvariant()
|
||||||
|
$isKnownPurpose = $false
|
||||||
|
foreach ($p in $kindPurposes.Keys) {
|
||||||
|
if ($p.ToLowerInvariant() -eq $Purpose.ToLowerInvariant()) { $isKnownPurpose = $true; break }
|
||||||
|
}
|
||||||
|
if (-not $isKnownPurpose -and $purposeSynonyms.ContainsKey($purposeProbe)) {
|
||||||
|
$Purpose = $purposeSynonyms[$purposeProbe]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (-not $Purpose) {
|
||||||
|
foreach ($p in $kindPurposes.Keys) {
|
||||||
|
if ($kindPurposes[$p].Primary) { $Purpose = $p; break }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$purposeKey = $null
|
||||||
|
foreach ($p in $kindPurposes.Keys) {
|
||||||
|
if ($p.ToLowerInvariant() -eq $Purpose.ToLowerInvariant()) { $purposeKey = $p; break }
|
||||||
|
}
|
||||||
|
if (-not $purposeKey) {
|
||||||
|
Write-Error "Назначение '$Purpose' недопустимо для $objectType. Допустимые: $(($kindPurposes.Keys | Sort-Object) -join ', ')"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
$Purpose = $purposeKey
|
||||||
|
$purposeRule = $kindPurposes[$Purpose]
|
||||||
|
|
||||||
$objectLikeTypes = @("Document", "Catalog", "ChartOfAccounts", "ChartOfCharacteristicTypes", "ExchangePlan", "BusinessProcess", "Task")
|
# Гард от повторения дефекта: запись таблицы обязана быть заполненной. Пустой MainAttr — это
|
||||||
$processorLikeTypes = @("DataProcessor", "Report", "ExternalDataProcessor", "ExternalReport")
|
# произвольная форма (законное состояние), а вот наполовину заполненная запись означала бы, что
|
||||||
|
# таблицу правили невнимательно, и в XML уйдёт мусор вроде `cfg:.Журнал`.
|
||||||
switch ($Purpose) {
|
if ($purposeRule.MainAttr -and -not $purposeRule.AttrName) {
|
||||||
"Object" {
|
Write-Error "Внутренняя ошибка таблицы видов: у $objectType/$Purpose задан MainAttr без AttrName"
|
||||||
# допустимо для всех типов
|
exit 1
|
||||||
}
|
|
||||||
"List" {
|
|
||||||
if ($objectType -eq "DataProcessor") {
|
|
||||||
Write-Error "Purpose=List недопустим для DataProcessor"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"Choice" {
|
|
||||||
if ($objectType -in $processorLikeTypes -or $objectType -eq "InformationRegister") {
|
|
||||||
Write-Error "Purpose=Choice недопустим для $objectType"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"Record" {
|
|
||||||
if ($objectType -ne "InformationRegister") {
|
|
||||||
Write-Error "Purpose=Record допустим только для InformationRegister"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Фаза 3: Создание файлов ---
|
# --- Фаза 3: Создание файлов ---
|
||||||
@@ -395,102 +557,47 @@ Write-XmlFile $formMetaPath $formMetaXml $encBom
|
|||||||
|
|
||||||
$formXmlPath = Join-Path $formExtDir "Form.xml"
|
$formXmlPath = Join-Path $formExtDir "Form.xml"
|
||||||
|
|
||||||
if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
|
# Одна ветка вместо трёх: что писать, решает запись таблицы видов. Раньше тип главного
|
||||||
# Динамический список
|
# реквизита брался из отдельной карты, и отсутствие вида в ней давало `cfg:.Имя` — молча.
|
||||||
# MainTable: тип.имя
|
$attributesBlock = ""
|
||||||
$mainTable = "$objectType.$objectName"
|
if ($purposeRule.MainAttr) {
|
||||||
|
$mainAttrType = $purposeRule.MainAttr -f $objectType, $objectName
|
||||||
|
$mainAttrName = $purposeRule.AttrName
|
||||||
|
|
||||||
$formXml = @"
|
# Динамический список несёт MainTable, остальные типы — SavedData по записи таблицы.
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
$tailLines = ""
|
||||||
<Form $($script:formNsDecl) version="$($script:formatVersion)">
|
if ($mainAttrType -eq "DynamicList") {
|
||||||
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
|
$mainTable = "$objectType.$objectName"
|
||||||
<Autofill>true</Autofill>
|
$tailLines = "`n`t`t`t<Settings xsi:type=""DynamicList"">`n`t`t`t`t<MainTable>$mainTable</MainTable>`n`t`t`t</Settings>"
|
||||||
</AutoCommandBar>
|
} elseif ($purposeRule.SavedData) {
|
||||||
<ChildItems/>
|
$tailLines = "`n`t`t`t<SavedData>true</SavedData>"
|
||||||
<Attributes>
|
}
|
||||||
<Attribute name="Список" id="1">
|
|
||||||
<Type>
|
$attributesBlock = @"
|
||||||
<v8:Type>cfg:DynamicList</v8:Type>
|
|
||||||
</Type>
|
|
||||||
<MainAttribute>true</MainAttribute>
|
|
||||||
<Settings xsi:type="DynamicList">
|
|
||||||
<MainTable>$mainTable</MainTable>
|
|
||||||
</Settings>
|
|
||||||
</Attribute>
|
|
||||||
</Attributes>
|
|
||||||
</Form>
|
|
||||||
"@
|
|
||||||
} elseif ($Purpose -eq "Record") {
|
|
||||||
# Запись регистра сведений
|
|
||||||
$mainAttrName = "Запись"
|
|
||||||
$mainAttrType = "InformationRegisterRecordManager.$objectName"
|
|
||||||
|
|
||||||
$formXml = @"
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Form $($script:formNsDecl) version="$($script:formatVersion)">
|
|
||||||
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
|
|
||||||
<Autofill>true</Autofill>
|
|
||||||
</AutoCommandBar>
|
|
||||||
<ChildItems/>
|
|
||||||
<Attributes>
|
<Attributes>
|
||||||
<Attribute name="$mainAttrName" id="1">
|
<Attribute name="$mainAttrName" id="1">
|
||||||
<Type>
|
<Type>
|
||||||
<v8:Type>cfg:$mainAttrType</v8:Type>
|
<v8:Type>cfg:$mainAttrType</v8:Type>
|
||||||
</Type>
|
</Type>
|
||||||
<MainAttribute>true</MainAttribute>
|
<MainAttribute>true</MainAttribute>$tailLines
|
||||||
<SavedData>true</SavedData>
|
|
||||||
</Attribute>
|
</Attribute>
|
||||||
</Attributes>
|
</Attributes>
|
||||||
</Form>
|
|
||||||
"@
|
|
||||||
} else {
|
|
||||||
# Object — форма объекта
|
|
||||||
$mainAttrName = "Объект"
|
|
||||||
|
|
||||||
# Маппинг типа объекта на тип реквизита
|
|
||||||
$attrTypeMap = @{
|
|
||||||
"Document" = "DocumentObject"
|
|
||||||
"Catalog" = "CatalogObject"
|
|
||||||
"DataProcessor" = "DataProcessorObject"
|
|
||||||
"Report" = "ReportObject"
|
|
||||||
"ExternalDataProcessor" = "ExternalDataProcessorObject"
|
|
||||||
"ExternalReport" = "ExternalReportObject"
|
|
||||||
"ChartOfAccounts" = "ChartOfAccountsObject"
|
|
||||||
"ChartOfCharacteristicTypes" = "ChartOfCharacteristicTypesObject"
|
|
||||||
"ExchangePlan" = "ExchangePlanObject"
|
|
||||||
"BusinessProcess" = "BusinessProcessObject"
|
|
||||||
"Task" = "TaskObject"
|
|
||||||
"InformationRegister" = "InformationRegisterRecordManager"
|
|
||||||
"AccumulationRegister" = "AccumulationRegisterRecordSet"
|
|
||||||
}
|
|
||||||
|
|
||||||
$mainAttrType = "$($attrTypeMap[$objectType]).$objectName"
|
|
||||||
|
|
||||||
# SavedData: standard for Catalog/Document/etc, but not for processor-like (DataProcessor/Report/External*)
|
|
||||||
$savedDataLine = ""
|
|
||||||
if ($objectType -notin $processorLikeTypes) {
|
|
||||||
$savedDataLine = "`n`t`t`t<SavedData>true</SavedData>"
|
|
||||||
}
|
|
||||||
|
|
||||||
$formXml = @"
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Form $($script:formNsDecl) version="$($script:formatVersion)">
|
|
||||||
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
|
|
||||||
<Autofill>true</Autofill>
|
|
||||||
</AutoCommandBar>
|
|
||||||
<ChildItems/>
|
|
||||||
<Attributes>
|
|
||||||
<Attribute name="$mainAttrName" id="1">
|
|
||||||
<Type>
|
|
||||||
<v8:Type>cfg:$mainAttrType</v8:Type>
|
|
||||||
</Type>
|
|
||||||
<MainAttribute>true</MainAttribute>$savedDataLine
|
|
||||||
</Attribute>
|
|
||||||
</Attributes>
|
|
||||||
</Form>
|
|
||||||
"@
|
"@
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Произвольная форма (MainAttr = $null) — без блока Attributes вовсе. В типовых это самая
|
||||||
|
# частая форма после объектной: 907 у справочников, 941 у документов, 3482 у отчётов.
|
||||||
|
$formXml = @"
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Form $($script:formNsDecl) version="$($script:formatVersion)">
|
||||||
|
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
|
||||||
|
<Autofill>true</Autofill>
|
||||||
|
</AutoCommandBar>
|
||||||
|
<ChildItems/>$attributesBlock
|
||||||
|
</Form>
|
||||||
|
"@
|
||||||
|
|
||||||
if (Test-Path $formXmlPath) {
|
if (Test-Path $formXmlPath) {
|
||||||
Write-Host "[SKIP] Form.xml already exists: $formXmlPath — not overwriting"
|
Write-Host "[SKIP] Form.xml already exists: $formXmlPath — not overwriting"
|
||||||
} else {
|
} else {
|
||||||
@@ -608,24 +715,17 @@ $isFirstFormForPurpose = $false
|
|||||||
$defaultPropName = $null
|
$defaultPropName = $null
|
||||||
$defaultValue = "$objectType.$objectName.Form.$FormName"
|
$defaultValue = "$objectType.$objectName.Form.$FormName"
|
||||||
|
|
||||||
# Определяем имя свойства для DefaultForm
|
# Свойство «основная форма» — из записи таблицы. Раньше выбиралось по одному Purpose без учёта
|
||||||
switch ($Purpose) {
|
# вида, и для журнала писалось DefaultListForm, которого у журнала нет: слот не находился, навык
|
||||||
"Object" {
|
# молча ничего не делал.
|
||||||
if ($objectType -in $processorLikeTypes) {
|
$defaultPropName = $purposeRule.Slot
|
||||||
$defaultPropName = "DefaultForm"
|
|
||||||
} else {
|
|
||||||
$defaultPropName = "DefaultObjectForm"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"List" { $defaultPropName = "DefaultListForm" }
|
|
||||||
"Choice" { $defaultPropName = "DefaultChoiceForm" }
|
|
||||||
"Record" { $defaultPropName = "DefaultRecordForm" }
|
|
||||||
}
|
|
||||||
|
|
||||||
# Проверяем, установлено ли уже значение
|
$defaultNode = $null
|
||||||
$defaultNode = $xmlDoc.SelectSingleNode("//md:${objectType}/md:Properties/md:$defaultPropName", $nsMgr)
|
if ($defaultPropName) {
|
||||||
if ($defaultNode) {
|
$defaultNode = $xmlDoc.SelectSingleNode("//md:${objectType}/md:Properties/md:$defaultPropName", $nsMgr)
|
||||||
$isFirstFormForPurpose = [string]::IsNullOrWhiteSpace($defaultNode.InnerText)
|
if ($defaultNode) {
|
||||||
|
$isFirstFormForPurpose = [string]::IsNullOrWhiteSpace($defaultNode.InnerText)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$defaultUpdated = $false
|
$defaultUpdated = $false
|
||||||
@@ -687,5 +787,9 @@ if ($alreadyRegistered) {
|
|||||||
}
|
}
|
||||||
if ($defaultUpdated) {
|
if ($defaultUpdated) {
|
||||||
Write-Host "${defaultPropName}: $defaultValue"
|
Write-Host "${defaultPropName}: $defaultValue"
|
||||||
|
} elseif (-not $defaultPropName) {
|
||||||
|
# Молчать здесь нельзя: пользователь ждёт, что форма станет основной, а свойства под неё
|
||||||
|
# у платформы нет (форма набора записей, произвольная форма).
|
||||||
|
Write-Host "Основной не назначена: у $objectType нет свойства для формы с назначением $Purpose"
|
||||||
}
|
}
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
+269
-137
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# form-add v1.25 — Add managed form to 1C config object (+write_xml_file/write_utf8_bom: общий эталон записи)
|
# form-add v1.28 — Add managed form to 1C config object (Python port)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -323,8 +323,13 @@ def main():
|
|||||||
parser.add_argument("-ObjectPath", required=True)
|
parser.add_argument("-ObjectPath", required=True)
|
||||||
parser.add_argument("-FormName", required=True)
|
parser.add_argument("-FormName", required=True)
|
||||||
parser.add_argument("-Synonym", default=None)
|
parser.add_argument("-Synonym", default=None)
|
||||||
parser.add_argument("-Purpose", default="Object")
|
# Пусто = основная форма вида (primary в таблице): у справочника это форма объекта,
|
||||||
parser.add_argument("-SetDefault", action="store_true")
|
# у регистра сведений — форма записи, у журнала — форма списка.
|
||||||
|
parser.add_argument("-Purpose", default="")
|
||||||
|
# Написания с дефисом внутри имени и с двойным дефисом: в PS-порте их принимает алиас
|
||||||
|
# set-default, здесь — перечисление опций, чтобы порты принимали ровно одно и то же.
|
||||||
|
parser.add_argument("-SetDefault", "--SetDefault", "--set-default", "-set-default",
|
||||||
|
dest="SetDefault", action="store_true")
|
||||||
args = ci_parse_args(parser)
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
object_path = args.ObjectPath
|
object_path = args.ObjectPath
|
||||||
@@ -415,24 +420,181 @@ def main():
|
|||||||
tree = etree.parse(object_xml_full, parser_xml)
|
tree = etree.parse(object_xml_full, parser_xml)
|
||||||
root = tree.getroot()
|
root = tree.getroot()
|
||||||
|
|
||||||
supported_types = [
|
# --- Таблица видов: вид -> допустимые назначения ---
|
||||||
"Document", "Catalog", "DataProcessor", "Report",
|
#
|
||||||
"ExternalDataProcessor", "ExternalReport",
|
# Зеркало $formKinds из PS-порта. Одна запись на вид вместо разрозненных списков
|
||||||
"InformationRegister", "AccumulationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
|
# «поддерживаемые типы», «объектные типы», «обработко-подобные» и «карта типов реквизита»:
|
||||||
"ExchangePlan", "BusinessProcess", "Task", "DocumentJournal",
|
# раньше они расходились молча, и для DocumentJournal в форму уходило `cfg:.Журнал`.
|
||||||
]
|
#
|
||||||
|
# main_attr — тип главного реквизита, {0} = вид, {1} = имя объекта;
|
||||||
|
# "DynamicList" — динамический список (добавляется Settings/MainTable);
|
||||||
|
# None — произвольная форма, блока Attributes нет вовсе.
|
||||||
|
# slot — свойство объекта под «основную форму»; None — такого свойства у вида нет.
|
||||||
|
# Эталон таблицы — docs/1c-form-spec.md, сверяется гардом check-form-purposes.mjs.
|
||||||
|
|
||||||
|
form_kinds = {
|
||||||
|
"Catalog": {
|
||||||
|
"Object": {"main_attr": "CatalogObject.{1}", "attr_name": "Объект",
|
||||||
|
"slot": "DefaultObjectForm", "saved_data": True, "primary": True},
|
||||||
|
"Folder": {"main_attr": "CatalogObject.{1}", "attr_name": "Объект",
|
||||||
|
"slot": "DefaultFolderForm", "saved_data": True},
|
||||||
|
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultListForm"},
|
||||||
|
"Choice": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultChoiceForm"},
|
||||||
|
"FolderChoice": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultFolderChoiceForm"},
|
||||||
|
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
|
||||||
|
},
|
||||||
|
"ChartOfCharacteristicTypes": {
|
||||||
|
"Object": {"main_attr": "ChartOfCharacteristicTypesObject.{1}", "attr_name": "Объект",
|
||||||
|
"slot": "DefaultObjectForm", "saved_data": True, "primary": True},
|
||||||
|
"Folder": {"main_attr": "ChartOfCharacteristicTypesObject.{1}", "attr_name": "Объект",
|
||||||
|
"slot": "DefaultFolderForm", "saved_data": True},
|
||||||
|
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultListForm"},
|
||||||
|
"Choice": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultChoiceForm"},
|
||||||
|
"FolderChoice": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultFolderChoiceForm"},
|
||||||
|
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
|
||||||
|
},
|
||||||
|
"Document": {
|
||||||
|
"Object": {"main_attr": "DocumentObject.{1}", "attr_name": "Объект",
|
||||||
|
"slot": "DefaultObjectForm", "saved_data": True, "primary": True},
|
||||||
|
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultListForm"},
|
||||||
|
"Choice": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultChoiceForm"},
|
||||||
|
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
|
||||||
|
},
|
||||||
|
"ChartOfAccounts": {
|
||||||
|
"Object": {"main_attr": "ChartOfAccountsObject.{1}", "attr_name": "Объект",
|
||||||
|
"slot": "DefaultObjectForm", "saved_data": True, "primary": True},
|
||||||
|
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultListForm"},
|
||||||
|
"Choice": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultChoiceForm"},
|
||||||
|
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
|
||||||
|
},
|
||||||
|
"ChartOfCalculationTypes": {
|
||||||
|
"Object": {"main_attr": "ChartOfCalculationTypesObject.{1}", "attr_name": "Объект",
|
||||||
|
"slot": "DefaultObjectForm", "saved_data": True, "primary": True},
|
||||||
|
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultListForm"},
|
||||||
|
"Choice": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultChoiceForm"},
|
||||||
|
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
|
||||||
|
},
|
||||||
|
"ExchangePlan": {
|
||||||
|
"Object": {"main_attr": "ExchangePlanObject.{1}", "attr_name": "Объект",
|
||||||
|
"slot": "DefaultObjectForm", "saved_data": True, "primary": True},
|
||||||
|
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultListForm"},
|
||||||
|
"Choice": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultChoiceForm"},
|
||||||
|
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
|
||||||
|
},
|
||||||
|
"BusinessProcess": {
|
||||||
|
"Object": {"main_attr": "BusinessProcessObject.{1}", "attr_name": "Объект",
|
||||||
|
"slot": "DefaultObjectForm", "saved_data": True, "primary": True},
|
||||||
|
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultListForm"},
|
||||||
|
"Choice": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultChoiceForm"},
|
||||||
|
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
|
||||||
|
},
|
||||||
|
"Task": {
|
||||||
|
"Object": {"main_attr": "TaskObject.{1}", "attr_name": "Объект",
|
||||||
|
"slot": "DefaultObjectForm", "saved_data": True, "primary": True},
|
||||||
|
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultListForm"},
|
||||||
|
"Choice": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultChoiceForm"},
|
||||||
|
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
|
||||||
|
},
|
||||||
|
"DataProcessor": {
|
||||||
|
"Object": {"main_attr": "DataProcessorObject.{1}", "attr_name": "Объект",
|
||||||
|
"slot": "DefaultForm", "primary": True},
|
||||||
|
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
|
||||||
|
},
|
||||||
|
"Report": {
|
||||||
|
"Object": {"main_attr": "ReportObject.{1}", "attr_name": "Объект",
|
||||||
|
"slot": "DefaultForm", "primary": True},
|
||||||
|
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
|
||||||
|
},
|
||||||
|
"ExternalDataProcessor": {
|
||||||
|
"Object": {"main_attr": "ExternalDataProcessorObject.{1}", "attr_name": "Объект",
|
||||||
|
"slot": "DefaultForm", "primary": True},
|
||||||
|
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
|
||||||
|
},
|
||||||
|
"ExternalReport": {
|
||||||
|
"Object": {"main_attr": "ExternalReportObject.{1}", "attr_name": "Объект",
|
||||||
|
"slot": "DefaultForm", "primary": True},
|
||||||
|
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
|
||||||
|
},
|
||||||
|
"InformationRegister": {
|
||||||
|
"Record": {"main_attr": "InformationRegisterRecordManager.{1}", "attr_name": "Запись",
|
||||||
|
"slot": "DefaultRecordForm", "saved_data": True, "primary": True},
|
||||||
|
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultListForm"},
|
||||||
|
"RecordSet": {"main_attr": "InformationRegisterRecordSet.{1}", "attr_name": "Набор",
|
||||||
|
"slot": None, "saved_data": True},
|
||||||
|
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
|
||||||
|
},
|
||||||
|
"AccumulationRegister": {
|
||||||
|
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultListForm", "primary": True},
|
||||||
|
"RecordSet": {"main_attr": "AccumulationRegisterRecordSet.{1}", "attr_name": "Набор",
|
||||||
|
"slot": None, "saved_data": True},
|
||||||
|
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
|
||||||
|
},
|
||||||
|
"AccountingRegister": {
|
||||||
|
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultListForm", "primary": True},
|
||||||
|
"RecordSet": {"main_attr": "AccountingRegisterRecordSet.{1}", "attr_name": "Набор",
|
||||||
|
"slot": None, "saved_data": True},
|
||||||
|
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
|
||||||
|
},
|
||||||
|
"CalculationRegister": {
|
||||||
|
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultListForm", "primary": True},
|
||||||
|
"RecordSet": {"main_attr": "CalculationRegisterRecordSet.{1}", "attr_name": "Набор",
|
||||||
|
"slot": None, "saved_data": True},
|
||||||
|
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
|
||||||
|
},
|
||||||
|
"DocumentJournal": {
|
||||||
|
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultForm", "primary": True},
|
||||||
|
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
|
||||||
|
},
|
||||||
|
"FilterCriterion": {
|
||||||
|
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultForm", "primary": True},
|
||||||
|
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
|
||||||
|
},
|
||||||
|
"Enum": {
|
||||||
|
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultListForm", "primary": True},
|
||||||
|
"Choice": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultChoiceForm"},
|
||||||
|
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
|
||||||
|
},
|
||||||
|
"SettingsStorage": {
|
||||||
|
"Save": {"main_attr": None, "attr_name": None, "slot": "DefaultSaveForm", "primary": True},
|
||||||
|
"Load": {"main_attr": None, "attr_name": None, "slot": "DefaultLoadForm"},
|
||||||
|
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Виды, у которых свойство DefaultForm есть, но собственных форм не бывает.
|
||||||
|
no_own_forms = {
|
||||||
|
"Constant": "у константы нет собственных форм — используйте общую форму (CommonForm)",
|
||||||
|
}
|
||||||
|
|
||||||
|
supported_types = list(form_kinds) + list(no_own_forms)
|
||||||
|
|
||||||
|
# Отдельный факт, не выводимый из таблицы назначений: у форм обработок и отчётов в
|
||||||
|
# метаданных формы есть <ExtendedPresentation>.
|
||||||
|
processor_like_types = ["DataProcessor", "Report", "ExternalDataProcessor", "ExternalReport"]
|
||||||
|
|
||||||
|
# Вид объекта — первый элемент-потомок MetaDataObject, а не первое совпавшее по всему
|
||||||
|
# документу имя. Поиск по документу зависел от порядка перебора видов: у бизнес-процесса
|
||||||
|
# есть свойство <Task>, и он определялся как задача, после чего имя объекта не находилось.
|
||||||
object_type = None
|
object_type = None
|
||||||
object_node = None
|
object_node = None
|
||||||
for t in supported_types:
|
for child in root:
|
||||||
node = root.find(f".//md:{t}", NSMAP)
|
if isinstance(child.tag, str):
|
||||||
if node is not None:
|
object_type = etree.QName(child).localname
|
||||||
object_type = t
|
object_node = child
|
||||||
object_node = node
|
|
||||||
break
|
break
|
||||||
|
|
||||||
|
if object_type is not None and object_type not in form_kinds and object_type not in no_own_forms:
|
||||||
|
print(f"Тип объекта '{object_type}' не поддерживается. "
|
||||||
|
f"Поддерживаемые типы: {', '.join(sorted(form_kinds))}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
if object_type is None:
|
if object_type is None:
|
||||||
print(f"Не удалось определить тип объекта. Поддерживаемые типы: {', '.join(supported_types)}", file=sys.stderr)
|
print(f"Не удалось определить тип объекта. Поддерживаемые типы: {', '.join(sorted(form_kinds))}",
|
||||||
|
file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if object_type in no_own_forms:
|
||||||
|
print(f"{object_type} не поддерживается: {no_own_forms[object_type]}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Object name from Properties/Name
|
# Object name from Properties/Name
|
||||||
@@ -449,32 +611,59 @@ def main():
|
|||||||
|
|
||||||
# --- Phase 2: Validate Purpose ---
|
# --- Phase 2: Validate Purpose ---
|
||||||
|
|
||||||
# Normalize: capitalize first letter, lowercase rest
|
# Назначение ищем в таблице регистронезависимо — как принимает PowerShell.
|
||||||
purpose = purpose[0].upper() + purpose[1:].lower()
|
kind_purposes = form_kinds[object_type]
|
||||||
|
|
||||||
valid_purposes = ["Object", "List", "Choice", "Record"]
|
# Обиходные написания назначения приводим к канону молча: русское название вида формы и
|
||||||
if purpose not in valid_purposes:
|
# английское с суффиксом Form. Ключ нормализуем — регистр, пробелы и разделители не значимы.
|
||||||
print(f"Недопустимое назначение: {purpose}. Допустимые: Object, List, Choice, Record", file=sys.stderr)
|
# Канон в документации один; здесь только приём ошибочного ввода, чтобы вызов не падал на форме
|
||||||
|
# записи вместо назначения. Применимость назначения к виду объекта проверяется ниже как обычно.
|
||||||
|
purpose_synonyms = {
|
||||||
|
"формаобъекта": "Object", "формаэлемента": "Object", "формадокумента": "Object",
|
||||||
|
"объект": "Object", "элемент": "Object", "документ": "Object", "objectform": "Object",
|
||||||
|
"формасписка": "List", "список": "List", "listform": "List",
|
||||||
|
"формавыбора": "Choice", "выбор": "Choice", "choiceform": "Choice",
|
||||||
|
"формагруппы": "Folder", "группа": "Folder", "folderform": "Folder",
|
||||||
|
"формавыборагруппы": "FolderChoice", "выборгруппы": "FolderChoice",
|
||||||
|
"folderchoiceform": "FolderChoice",
|
||||||
|
"формазаписи": "Record", "запись": "Record", "recordform": "Record",
|
||||||
|
"форманаборазаписей": "RecordSet", "наборзаписей": "RecordSet", "recordsetform": "RecordSet",
|
||||||
|
"формасохранения": "Save", "формасохранениянастроек": "Save", "сохранение": "Save",
|
||||||
|
"saveform": "Save",
|
||||||
|
"формазагрузки": "Load", "формазагрузкинастроек": "Load", "загрузка": "Load",
|
||||||
|
"loadform": "Load",
|
||||||
|
"произвольная": "Custom", "произвольнаяформа": "Custom", "customform": "Custom",
|
||||||
|
}
|
||||||
|
if purpose:
|
||||||
|
purpose_probe = re.sub(r"[\s_-]", "", purpose).lower()
|
||||||
|
is_known_purpose = any(k.lower() == purpose.lower() for k in kind_purposes)
|
||||||
|
if not is_known_purpose and purpose_probe in purpose_synonyms:
|
||||||
|
purpose = purpose_synonyms[purpose_probe]
|
||||||
|
|
||||||
|
if not purpose:
|
||||||
|
for k, rule in kind_purposes.items():
|
||||||
|
if rule.get("primary"):
|
||||||
|
purpose = k
|
||||||
|
break
|
||||||
|
purpose_key = None
|
||||||
|
for k in kind_purposes:
|
||||||
|
if k.lower() == purpose.lower():
|
||||||
|
purpose_key = k
|
||||||
|
break
|
||||||
|
if purpose_key is None:
|
||||||
|
print(f"Назначение '{purpose}' недопустимо для {object_type}. "
|
||||||
|
f"Допустимые: {', '.join(sorted(kind_purposes))}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
purpose = purpose_key
|
||||||
|
purpose_rule = kind_purposes[purpose]
|
||||||
|
|
||||||
object_like_types = ["Document", "Catalog", "ChartOfAccounts", "ChartOfCharacteristicTypes",
|
# Гард от повторения дефекта: запись таблицы обязана быть заполненной. Пустой main_attr —
|
||||||
"ExchangePlan", "BusinessProcess", "Task"]
|
# это произвольная форма (законное состояние), а наполовину заполненная запись означала бы,
|
||||||
processor_like_types = ["DataProcessor", "Report", "ExternalDataProcessor", "ExternalReport"]
|
# что таблицу правили невнимательно, и в XML уйдёт мусор вроде `cfg:.Журнал`.
|
||||||
|
if purpose_rule.get("main_attr") and not purpose_rule.get("attr_name"):
|
||||||
if purpose == "List":
|
print(f"Внутренняя ошибка таблицы видов: у {object_type}/{purpose} задан main_attr без attr_name",
|
||||||
if object_type == "DataProcessor":
|
file=sys.stderr)
|
||||||
print("Purpose=List недопустим для DataProcessor", file=sys.stderr)
|
sys.exit(1)
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
elif purpose == "Choice":
|
|
||||||
if object_type in processor_like_types or object_type == "InformationRegister":
|
|
||||||
print(f"Purpose=Choice недопустим для {object_type}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
elif purpose == "Record":
|
|
||||||
if object_type != "InformationRegister":
|
|
||||||
print("Purpose=Record допустим только для InformationRegister", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Phase 3: Create files ---
|
# --- Phase 3: Create files ---
|
||||||
|
|
||||||
@@ -531,100 +720,47 @@ def main():
|
|||||||
|
|
||||||
form_xml_path = os.path.join(form_ext_dir, "Form.xml")
|
form_xml_path = os.path.join(form_ext_dir, "Form.xml")
|
||||||
|
|
||||||
if purpose in ("List", "Choice"):
|
# Одна ветка вместо трёх: что писать, решает запись таблицы видов. Раньше тип главного
|
||||||
# Dynamic list
|
# реквизита брался из отдельной карты, и отсутствие вида в ней давало `cfg:.Имя` — молча.
|
||||||
main_table = f"{object_type}.{object_name}"
|
attributes_block = ''
|
||||||
|
if purpose_rule.get("main_attr"):
|
||||||
|
main_attr_type = purpose_rule["main_attr"].format(object_type, object_name)
|
||||||
|
main_attr_name = purpose_rule["attr_name"]
|
||||||
|
|
||||||
form_xml = (
|
# Динамический список несёт MainTable, остальные типы — SavedData по записи таблицы.
|
||||||
f'<?xml version="1.0" encoding="UTF-8"?>\n'
|
tail_lines = ''
|
||||||
f'<Form {form_ns_decl} version="{format_version}">\n'
|
if main_attr_type == "DynamicList":
|
||||||
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
|
main_table = f"{object_type}.{object_name}"
|
||||||
'\t\t<Autofill>true</Autofill>\n'
|
tail_lines = ('\t\t\t<Settings xsi:type="DynamicList">\n'
|
||||||
'\t</AutoCommandBar>\n'
|
f'\t\t\t\t<MainTable>{main_table}</MainTable>\n'
|
||||||
'\t<ChildItems/>\n'
|
'\t\t\t</Settings>\n')
|
||||||
'\t<Attributes>\n'
|
elif purpose_rule.get("saved_data"):
|
||||||
'\t\t<Attribute name="\u0421\u043f\u0438\u0441\u043e\u043a" id="1">\n'
|
tail_lines = '\t\t\t<SavedData>true</SavedData>\n'
|
||||||
'\t\t\t<Type>\n'
|
|
||||||
'\t\t\t\t<v8:Type>cfg:DynamicList</v8:Type>\n'
|
|
||||||
'\t\t\t</Type>\n'
|
|
||||||
'\t\t\t<MainAttribute>true</MainAttribute>\n'
|
|
||||||
'\t\t\t<Settings xsi:type="DynamicList">\n'
|
|
||||||
f'\t\t\t\t<MainTable>{main_table}</MainTable>\n'
|
|
||||||
'\t\t\t</Settings>\n'
|
|
||||||
'\t\t</Attribute>\n'
|
|
||||||
'\t</Attributes>\n'
|
|
||||||
'</Form>'
|
|
||||||
)
|
|
||||||
|
|
||||||
elif purpose == "Record":
|
attributes_block = (
|
||||||
# Information register record
|
|
||||||
main_attr_name = "\u0417\u0430\u043f\u0438\u0441\u044c"
|
|
||||||
main_attr_type = f"InformationRegisterRecordManager.{object_name}"
|
|
||||||
|
|
||||||
form_xml = (
|
|
||||||
f'<?xml version="1.0" encoding="UTF-8"?>\n'
|
|
||||||
f'<Form {form_ns_decl} version="{format_version}">\n'
|
|
||||||
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
|
|
||||||
'\t\t<Autofill>true</Autofill>\n'
|
|
||||||
'\t</AutoCommandBar>\n'
|
|
||||||
'\t<ChildItems/>\n'
|
|
||||||
'\t<Attributes>\n'
|
'\t<Attributes>\n'
|
||||||
f'\t\t<Attribute name="{main_attr_name}" id="1">\n'
|
f'\t\t<Attribute name="{main_attr_name}" id="1">\n'
|
||||||
'\t\t\t<Type>\n'
|
'\t\t\t<Type>\n'
|
||||||
f'\t\t\t\t<v8:Type>cfg:{main_attr_type}</v8:Type>\n'
|
f'\t\t\t\t<v8:Type>cfg:{main_attr_type}</v8:Type>\n'
|
||||||
'\t\t\t</Type>\n'
|
'\t\t\t</Type>\n'
|
||||||
'\t\t\t<MainAttribute>true</MainAttribute>\n'
|
'\t\t\t<MainAttribute>true</MainAttribute>\n'
|
||||||
'\t\t\t<SavedData>true</SavedData>\n'
|
f'{tail_lines}'
|
||||||
'\t\t</Attribute>\n'
|
'\t\t</Attribute>\n'
|
||||||
'\t</Attributes>\n'
|
'\t</Attributes>\n'
|
||||||
'</Form>'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
# Произвольная форма (main_attr=None) — без блока Attributes вовсе. В типовых это самая
|
||||||
# Object — object form
|
# частая форма после объектной: 907 у справочников, 941 у документов, 3482 у отчётов.
|
||||||
main_attr_name = "\u041e\u0431\u044a\u0435\u043a\u0442"
|
form_xml = (
|
||||||
|
f'<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||||
attr_type_map = {
|
f'<Form {form_ns_decl} version="{format_version}">\n'
|
||||||
"Document": "DocumentObject",
|
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
|
||||||
"Catalog": "CatalogObject",
|
'\t\t<Autofill>true</Autofill>\n'
|
||||||
"DataProcessor": "DataProcessorObject",
|
'\t</AutoCommandBar>\n'
|
||||||
"Report": "ReportObject",
|
'\t<ChildItems/>\n'
|
||||||
"ExternalDataProcessor": "ExternalDataProcessorObject",
|
f'{attributes_block}'
|
||||||
"ExternalReport": "ExternalReportObject",
|
'</Form>'
|
||||||
"ChartOfAccounts": "ChartOfAccountsObject",
|
)
|
||||||
"ChartOfCharacteristicTypes": "ChartOfCharacteristicTypesObject",
|
|
||||||
"ExchangePlan": "ExchangePlanObject",
|
|
||||||
"BusinessProcess": "BusinessProcessObject",
|
|
||||||
"Task": "TaskObject",
|
|
||||||
"InformationRegister": "InformationRegisterRecordManager",
|
|
||||||
"AccumulationRegister": "AccumulationRegisterRecordSet",
|
|
||||||
}
|
|
||||||
|
|
||||||
main_attr_type = f"{attr_type_map[object_type]}.{object_name}"
|
|
||||||
|
|
||||||
# SavedData: standard for Catalog/Document/etc, but not for processor-like (DataProcessor/Report/External*)
|
|
||||||
saved_data_line = ''
|
|
||||||
if object_type not in processor_like_types:
|
|
||||||
saved_data_line = '\t\t\t<SavedData>true</SavedData>\n'
|
|
||||||
|
|
||||||
form_xml = (
|
|
||||||
f'<?xml version="1.0" encoding="UTF-8"?>\n'
|
|
||||||
f'<Form {form_ns_decl} version="{format_version}">\n'
|
|
||||||
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
|
|
||||||
'\t\t<Autofill>true</Autofill>\n'
|
|
||||||
'\t</AutoCommandBar>\n'
|
|
||||||
'\t<ChildItems/>\n'
|
|
||||||
'\t<Attributes>\n'
|
|
||||||
f'\t\t<Attribute name="{main_attr_name}" id="1">\n'
|
|
||||||
'\t\t\t<Type>\n'
|
|
||||||
f'\t\t\t\t<v8:Type>cfg:{main_attr_type}</v8:Type>\n'
|
|
||||||
'\t\t\t</Type>\n'
|
|
||||||
'\t\t\t<MainAttribute>true</MainAttribute>\n'
|
|
||||||
f'{saved_data_line}'
|
|
||||||
'\t\t</Attribute>\n'
|
|
||||||
'\t</Attributes>\n'
|
|
||||||
'</Form>'
|
|
||||||
)
|
|
||||||
|
|
||||||
if os.path.exists(form_xml_path):
|
if os.path.exists(form_xml_path):
|
||||||
print(f"[SKIP] Form.xml already exists: {form_xml_path} — not overwriting")
|
print(f"[SKIP] Form.xml already exists: {form_xml_path} — not overwriting")
|
||||||
@@ -721,26 +857,18 @@ def main():
|
|||||||
# --- SetDefault ---
|
# --- SetDefault ---
|
||||||
|
|
||||||
is_first_form_for_purpose = False
|
is_first_form_for_purpose = False
|
||||||
default_prop_name = None
|
|
||||||
default_value = f"{object_type}.{object_name}.Form.{form_name}"
|
default_value = f"{object_type}.{object_name}.Form.{form_name}"
|
||||||
|
|
||||||
# Determine property name for DefaultForm
|
# Свойство «основная форма» — из записи таблицы. Раньше выбиралось по одному purpose без
|
||||||
if purpose == "Object":
|
# учёта вида, и для журнала писалось DefaultListForm, которого у журнала нет: слот не
|
||||||
if object_type in processor_like_types:
|
# находился, навык молча ничего не делал.
|
||||||
default_prop_name = "DefaultForm"
|
default_prop_name = purpose_rule.get("slot")
|
||||||
else:
|
|
||||||
default_prop_name = "DefaultObjectForm"
|
|
||||||
elif purpose == "List":
|
|
||||||
default_prop_name = "DefaultListForm"
|
|
||||||
elif purpose == "Choice":
|
|
||||||
default_prop_name = "DefaultChoiceForm"
|
|
||||||
elif purpose == "Record":
|
|
||||||
default_prop_name = "DefaultRecordForm"
|
|
||||||
|
|
||||||
# Check if value is already set
|
default_node = None
|
||||||
default_node = root.find(f".//md:{object_type}/md:Properties/md:{default_prop_name}", NSMAP)
|
if default_prop_name:
|
||||||
if default_node is not None:
|
default_node = root.find(f".//md:{object_type}/md:Properties/md:{default_prop_name}", NSMAP)
|
||||||
is_first_form_for_purpose = default_node.text is None or default_node.text.strip() == ""
|
if default_node is not None:
|
||||||
|
is_first_form_for_purpose = not (default_node.text or "").strip()
|
||||||
|
|
||||||
default_updated = False
|
default_updated = False
|
||||||
if set_default or is_first_form_for_purpose:
|
if set_default or is_first_form_for_purpose:
|
||||||
@@ -767,6 +895,10 @@ def main():
|
|||||||
print(f"Registered: <Form>{form_name}</Form> in ChildObjects")
|
print(f"Registered: <Form>{form_name}</Form> in ChildObjects")
|
||||||
if default_updated:
|
if default_updated:
|
||||||
print(f"{default_prop_name}: {default_value}")
|
print(f"{default_prop_name}: {default_value}")
|
||||||
|
elif not default_prop_name:
|
||||||
|
# Молчать здесь нельзя: пользователь ждёт, что форма станет основной, а свойства под неё
|
||||||
|
# у платформы нет (форма набора записей, произвольная форма).
|
||||||
|
print(f"Основной не назначена: у {object_type} нет свойства для формы с назначением {purpose}")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
|
|
||||||
@@ -29,10 +29,10 @@ allowed-tools:
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Режим JSON DSL
|
# Режим JSON DSL
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-compile.ps1" -JsonPath "<json>" -OutputPath "<Form.xml>"
|
python ".gemini/skills/form-compile/scripts/form-compile.py" -JsonPath "<json>" -OutputPath "<Form.xml>"
|
||||||
|
|
||||||
# Режим from-object (объект и purpose выводятся из OutputPath; Document и Catalog)
|
# Режим from-object (объект и purpose выводятся из OutputPath; Document и Catalog)
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-compile.ps1" -FromObject -OutputPath "<.../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml>"
|
python ".gemini/skills/form-compile/scripts/form-compile.py" -FromObject -OutputPath "<.../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml>"
|
||||||
```
|
```
|
||||||
|
|
||||||
## JSON DSL — справка
|
## JSON DSL — справка
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user