mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-09-11 20:50:52 +03:00
Compare commits
246
Commits
@@ -1,4 +1,4 @@
|
|||||||
# cf-edit v1.7 — Edit 1C configuration root (Configuration.xml)
|
# cf-edit v1.11 — Edit 1C configuration root (Configuration.xml)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)][Alias('Path')][string]$ConfigPath,
|
[Parameter(Mandatory)][Alias('Path')][string]$ConfigPath,
|
||||||
@@ -44,6 +44,16 @@ function Get-RootUuid([string]$xmlPath) {
|
|||||||
} catch {}
|
} catch {}
|
||||||
return $null
|
return $null
|
||||||
}
|
}
|
||||||
|
function Test-ExternalObjectRoot([string]$xmlPath) {
|
||||||
|
if (-not (Test-Path $xmlPath)) { return $false }
|
||||||
|
try {
|
||||||
|
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||||
|
$el = $mx.DocumentElement.FirstChild
|
||||||
|
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||||
|
if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
|
||||||
|
} catch {}
|
||||||
|
return $false
|
||||||
|
}
|
||||||
function Find-V8Project([string]$startDir) {
|
function Find-V8Project([string]$startDir) {
|
||||||
$d = $startDir
|
$d = $startDir
|
||||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||||
@@ -80,10 +90,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
|||||||
try {
|
try {
|
||||||
$rp = $targetPath
|
$rp = $targetPath
|
||||||
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
||||||
|
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||||
|
if (Test-ExternalObjectRoot $rp) { return }
|
||||||
$elemUuid = Get-RootUuid $rp
|
$elemUuid = Get-RootUuid $rp
|
||||||
$cfgDir = $null; $binPath = $null
|
$cfgDir = $null; $binPath = $null
|
||||||
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
||||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||||
|
if (Test-ExternalObjectRoot "$d.xml") { return }
|
||||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||||
if (-not $cfgDir) {
|
if (-not $cfgDir) {
|
||||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||||
@@ -154,6 +167,11 @@ $script:xmlDoc = New-Object System.Xml.XmlDocument
|
|||||||
$script:xmlDoc.PreserveWhitespace = $true
|
$script:xmlDoc.PreserveWhitespace = $true
|
||||||
$script:xmlDoc.Load($resolvedPath)
|
$script:xmlDoc.Load($resolvedPath)
|
||||||
|
|
||||||
|
# Версия формата редактируемой конфигурации — создаваемые рядом файлы (Ext/HomePageWorkArea.xml)
|
||||||
|
# должны нести ту же версию, иначе в проекте окажутся файлы разных версий формата.
|
||||||
|
$script:formatVersion = $script:xmlDoc.DocumentElement.GetAttribute("version")
|
||||||
|
if (-not $script:formatVersion) { $script:formatVersion = "2.17" }
|
||||||
|
|
||||||
$script:addCount = 0
|
$script:addCount = 0
|
||||||
$script:removeCount = 0
|
$script:removeCount = 0
|
||||||
$script:modifyCount = 0
|
$script:modifyCount = 0
|
||||||
@@ -196,7 +214,7 @@ Info "Configuration: $($script:objName)"
|
|||||||
$script:typeOrder = @(
|
$script:typeOrder = @(
|
||||||
"Language","Subsystem","StyleItem","Style",
|
"Language","Subsystem","StyleItem","Style",
|
||||||
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
||||||
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
|
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
|
||||||
"XDTOPackage","WebService","HTTPService","WSReference",
|
"XDTOPackage","WebService","HTTPService","WSReference",
|
||||||
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
||||||
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
||||||
@@ -212,7 +230,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"; "CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"
|
"FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"; "Bot"="Bots"; "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"
|
||||||
@@ -700,6 +718,7 @@ $script:ruTypeMap = @{
|
|||||||
"регистррасчёта" = "CalculationRegister"
|
"регистррасчёта" = "CalculationRegister"
|
||||||
"бизнеспроцесс" = "BusinessProcess"
|
"бизнеспроцесс" = "BusinessProcess"
|
||||||
"задача" = "Task"
|
"задача" = "Task"
|
||||||
|
"бот" = "Bot"
|
||||||
"планобмена" = "ExchangePlan"
|
"планобмена" = "ExchangePlan"
|
||||||
"хранилищенастроек" = "SettingsStorage"
|
"хранилищенастроек" = "SettingsStorage"
|
||||||
}
|
}
|
||||||
@@ -850,7 +869,7 @@ function Do-SetHomePage($valArg) {
|
|||||||
|
|
||||||
$hpXml = @"
|
$hpXml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<HomePageWorkArea xmlns="http://v8.1c.ru/8.3/xcf/extrnprops" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
<HomePageWorkArea xmlns="http://v8.1c.ru/8.3/xcf/extrnprops" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$($script:formatVersion)">
|
||||||
<WorkingAreaTemplate>$tmpl</WorkingAreaTemplate>
|
<WorkingAreaTemplate>$tmpl</WorkingAreaTemplate>
|
||||||
$leftXml
|
$leftXml
|
||||||
$rightXml
|
$rightXml
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cf-edit v1.7 — Edit 1C configuration root (Configuration.xml)
|
# cf-edit v1.11 — 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
|
||||||
@@ -33,6 +33,18 @@ def _sg_root_uuid(xml_path):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _sg_is_external_root(xml_path):
|
||||||
|
if not os.path.isfile(xml_path):
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
mx = etree.parse(xml_path).getroot()
|
||||||
|
for child in mx:
|
||||||
|
if isinstance(child.tag, str):
|
||||||
|
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
return False
|
||||||
|
|
||||||
def _sg_find_v8project(start_dir):
|
def _sg_find_v8project(start_dir):
|
||||||
d = start_dir
|
d = start_dir
|
||||||
for _ in range(20):
|
for _ in range(20):
|
||||||
@@ -72,6 +84,9 @@ def _sg_get_edit_mode(cfg_dir):
|
|||||||
def assert_edit_allowed(target_path, require):
|
def assert_edit_allowed(target_path, require):
|
||||||
try:
|
try:
|
||||||
rp = os.path.abspath(target_path)
|
rp = os.path.abspath(target_path)
|
||||||
|
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||||
|
if _sg_is_external_root(rp):
|
||||||
|
return
|
||||||
elem_uuid = _sg_root_uuid(rp)
|
elem_uuid = _sg_root_uuid(rp)
|
||||||
cfg_dir = None
|
cfg_dir = None
|
||||||
bin_path = None
|
bin_path = None
|
||||||
@@ -79,6 +94,8 @@ def assert_edit_allowed(target_path, require):
|
|||||||
for _ in range(12):
|
for _ in range(12):
|
||||||
if not d:
|
if not d:
|
||||||
break
|
break
|
||||||
|
if _sg_is_external_root(d + ".xml"):
|
||||||
|
return
|
||||||
if not elem_uuid:
|
if not elem_uuid:
|
||||||
elem_uuid = _sg_root_uuid(d + ".xml")
|
elem_uuid = _sg_root_uuid(d + ".xml")
|
||||||
if not cfg_dir:
|
if not cfg_dir:
|
||||||
@@ -182,7 +199,7 @@ XS_NS = "http://www.w3.org/2001/XMLSchema"
|
|||||||
TYPE_ORDER = [
|
TYPE_ORDER = [
|
||||||
"Language", "Subsystem", "StyleItem", "Style",
|
"Language", "Subsystem", "StyleItem", "Style",
|
||||||
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
|
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
|
||||||
"FilterCriterion", "CommonModule", "CommonAttribute", "ExchangePlan",
|
"FilterCriterion", "CommonModule", "Bot", "CommonAttribute", "ExchangePlan",
|
||||||
"XDTOPackage", "WebService", "HTTPService", "WSReference",
|
"XDTOPackage", "WebService", "HTTPService", "WSReference",
|
||||||
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
|
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
|
||||||
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
|
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
|
||||||
@@ -198,7 +215,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", "CommonAttribute": "CommonAttributes", "ExchangePlan": "ExchangePlans",
|
"FilterCriterion": "FilterCriteria", "CommonModule": "CommonModules", "Bot": "Bots", "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",
|
||||||
@@ -307,13 +324,49 @@ def parse_batch_value(val):
|
|||||||
return items
|
return items
|
||||||
|
|
||||||
|
|
||||||
def save_xml_bom(tree, path):
|
def _detect_xml_style(path):
|
||||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
"""Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
|
||||||
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
финальный перенос. None → файл новый (сохранить текущее поведение)."""
|
||||||
if not xml_bytes.endswith(b"\n"):
|
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)."""
|
||||||
|
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"))
|
||||||
|
# Финальный перенос — как в оригинале (новый файл → есть)
|
||||||
|
want_final_nl = style["final_nl"] if style else True
|
||||||
|
xml_bytes = xml_bytes.rstrip(b"\n")
|
||||||
|
if want_final_nl:
|
||||||
xml_bytes += b"\n"
|
xml_bytes += b"\n"
|
||||||
|
# EOL — как в оригинале (новый файл → LF, текущее поведение)
|
||||||
|
if style and style["crlf"]:
|
||||||
|
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
|
||||||
|
return xml_bytes
|
||||||
|
|
||||||
|
|
||||||
|
def save_xml_bom(tree, path):
|
||||||
|
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:
|
with open(path, "wb") as f:
|
||||||
f.write(b"\xef\xbb\xbf")
|
if style is None or style["bom"]:
|
||||||
|
f.write(b"\xef\xbb\xbf")
|
||||||
f.write(xml_bytes)
|
f.write(xml_bytes)
|
||||||
|
|
||||||
|
|
||||||
@@ -357,6 +410,10 @@ def main():
|
|||||||
tree = etree.parse(resolved_path, xml_parser)
|
tree = etree.parse(resolved_path, xml_parser)
|
||||||
xml_root = tree.getroot()
|
xml_root = tree.getroot()
|
||||||
|
|
||||||
|
# Версия формата редактируемой конфигурации — создаваемые рядом файлы (Ext/HomePageWorkArea.xml)
|
||||||
|
# должны нести ту же версию, иначе в проекте окажутся файлы разных версий формата.
|
||||||
|
format_version = xml_root.get('version') or '2.17'
|
||||||
|
|
||||||
add_count = 0
|
add_count = 0
|
||||||
remove_count = 0
|
remove_count = 0
|
||||||
modify_count = 0
|
modify_count = 0
|
||||||
@@ -771,6 +828,7 @@ def main():
|
|||||||
"регистррасчета": "CalculationRegister",
|
"регистррасчета": "CalculationRegister",
|
||||||
"регистррасчёта": "CalculationRegister",
|
"регистррасчёта": "CalculationRegister",
|
||||||
"бизнеспроцесс": "BusinessProcess",
|
"бизнеспроцесс": "BusinessProcess",
|
||||||
|
"бот": "Bot",
|
||||||
"задача": "Task", "планобмена": "ExchangePlan",
|
"задача": "Task", "планобмена": "ExchangePlan",
|
||||||
"хранилищенастроек": "SettingsStorage",
|
"хранилищенастроек": "SettingsStorage",
|
||||||
}
|
}
|
||||||
@@ -905,7 +963,7 @@ def main():
|
|||||||
'<HomePageWorkArea xmlns="http://v8.1c.ru/8.3/xcf/extrnprops" '
|
'<HomePageWorkArea xmlns="http://v8.1c.ru/8.3/xcf/extrnprops" '
|
||||||
'xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" '
|
'xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" '
|
||||||
'xmlns:xs="http://www.w3.org/2001/XMLSchema" '
|
'xmlns:xs="http://www.w3.org/2001/XMLSchema" '
|
||||||
'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">\r\n'
|
f'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{format_version}">\r\n'
|
||||||
f'\t<WorkingAreaTemplate>{tmpl}</WorkingAreaTemplate>\r\n'
|
f'\t<WorkingAreaTemplate>{tmpl}</WorkingAreaTemplate>\r\n'
|
||||||
f'{left_xml}\r\n'
|
f'{left_xml}\r\n'
|
||||||
f'{right_xml}\r\n'
|
f'{right_xml}\r\n'
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# cf-info v1.3 — Compact summary of 1C configuration root
|
# cf-info v1.4 — Compact summary of 1C configuration root
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory=$true)][Alias('Path')][string]$ConfigPath,
|
[Parameter(Mandatory=$true)][Alias('Path')][string]$ConfigPath,
|
||||||
@@ -89,7 +89,7 @@ function Get-PropML([string]$propName) {
|
|||||||
$typeOrder = @(
|
$typeOrder = @(
|
||||||
"Language","Subsystem","StyleItem","Style",
|
"Language","Subsystem","StyleItem","Style",
|
||||||
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
||||||
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
|
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
|
||||||
"XDTOPackage","WebService","HTTPService","WSReference",
|
"XDTOPackage","WebService","HTTPService","WSReference",
|
||||||
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
||||||
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
||||||
@@ -105,6 +105,7 @@ $typeRuNames = @{
|
|||||||
"Language"="Языки"; "Subsystem"="Подсистемы"; "StyleItem"="Элементы стиля"; "Style"="Стили"
|
"Language"="Языки"; "Subsystem"="Подсистемы"; "StyleItem"="Элементы стиля"; "Style"="Стили"
|
||||||
"CommonPicture"="Общие картинки"; "SessionParameter"="Параметры сеанса"; "Role"="Роли"
|
"CommonPicture"="Общие картинки"; "SessionParameter"="Параметры сеанса"; "Role"="Роли"
|
||||||
"CommonTemplate"="Общие макеты"; "FilterCriterion"="Критерии отбора"; "CommonModule"="Общие модули"
|
"CommonTemplate"="Общие макеты"; "FilterCriterion"="Критерии отбора"; "CommonModule"="Общие модули"
|
||||||
|
"Bot"="Боты"
|
||||||
"CommonAttribute"="Общие реквизиты"; "ExchangePlan"="Планы обмена"; "XDTOPackage"="XDTO-пакеты"
|
"CommonAttribute"="Общие реквизиты"; "ExchangePlan"="Планы обмена"; "XDTOPackage"="XDTO-пакеты"
|
||||||
"WebService"="Веб-сервисы"; "HTTPService"="HTTP-сервисы"; "WSReference"="WS-ссылки"
|
"WebService"="Веб-сервисы"; "HTTPService"="HTTP-сервисы"; "WSReference"="WS-ссылки"
|
||||||
"EventSubscription"="Подписки на события"; "ScheduledJob"="Регламентные задания"
|
"EventSubscription"="Подписки на события"; "ScheduledJob"="Регламентные задания"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cf-info v1.3 — Compact summary of 1C configuration root
|
# cf-info v1.4 — 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
|
||||||
@@ -95,7 +95,7 @@ def get_prop_ml(prop_name):
|
|||||||
type_order = [
|
type_order = [
|
||||||
"Language", "Subsystem", "StyleItem", "Style",
|
"Language", "Subsystem", "StyleItem", "Style",
|
||||||
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
|
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
|
||||||
"FilterCriterion", "CommonModule", "CommonAttribute", "ExchangePlan",
|
"FilterCriterion", "CommonModule", "Bot", "CommonAttribute", "ExchangePlan",
|
||||||
"XDTOPackage", "WebService", "HTTPService", "WSReference",
|
"XDTOPackage", "WebService", "HTTPService", "WSReference",
|
||||||
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
|
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
|
||||||
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
|
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
|
||||||
@@ -111,6 +111,7 @@ type_ru_names = {
|
|||||||
"Language": "Языки", "Subsystem": "Подсистемы", "StyleItem": "Элементы стиля", "Style": "Стили",
|
"Language": "Языки", "Subsystem": "Подсистемы", "StyleItem": "Элементы стиля", "Style": "Стили",
|
||||||
"CommonPicture": "Общие картинки", "SessionParameter": "Параметры сеанса", "Role": "Роли",
|
"CommonPicture": "Общие картинки", "SessionParameter": "Параметры сеанса", "Role": "Роли",
|
||||||
"CommonTemplate": "Общие макеты", "FilterCriterion": "Критерии отбора", "CommonModule": "Общие модули",
|
"CommonTemplate": "Общие макеты", "FilterCriterion": "Критерии отбора", "CommonModule": "Общие модули",
|
||||||
|
"Bot": "Боты",
|
||||||
"CommonAttribute": "Общие реквизиты", "ExchangePlan": "Планы обмена", "XDTOPackage": "XDTO-пакеты",
|
"CommonAttribute": "Общие реквизиты", "ExchangePlan": "Планы обмена", "XDTOPackage": "XDTO-пакеты",
|
||||||
"WebService": "Веб-сервисы", "HTTPService": "HTTP-сервисы", "WSReference": "WS-ссылки",
|
"WebService": "Веб-сервисы", "HTTPService": "HTTP-сервисы", "WSReference": "WS-ссылки",
|
||||||
"EventSubscription": "Подписки на события", "ScheduledJob": "Регламентные задания",
|
"EventSubscription": "Подписки на события", "ScheduledJob": "Регламентные задания",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# cf-init v1.2 — Create empty 1C configuration scaffold
|
# cf-init v1.4 — Create empty 1C configuration scaffold
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -7,7 +7,12 @@ param(
|
|||||||
[string]$OutputDir = "src",
|
[string]$OutputDir = "src",
|
||||||
[string]$Version,
|
[string]$Version,
|
||||||
[string]$Vendor,
|
[string]$Vendor,
|
||||||
[string]$CompatibilityMode = "Version8_3_24"
|
[string]$CompatibilityMode = "Version8_3_24",
|
||||||
|
# Версия формата выгрузки (MDClasses). Её задаёт ПЛАТФОРМА, которой выгружают, и от режима
|
||||||
|
# совместимости она не зависит: 8.3.20-8.3.24 пишут 2.17, 8.3.25 — 2.18, 8.3.26 — 2.19,
|
||||||
|
# 8.3.27 — 2.20. Дефолт консервативный: 2.17 читается всеми поддерживаемыми платформами.
|
||||||
|
[ValidateSet("2.17", "2.18", "2.19", "2.20", "2.21")]
|
||||||
|
[string]$FormatVersion = "2.17"
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
@@ -73,7 +78,7 @@ $versionXml = if ($Version) { [System.Security.SecurityElement]::Escape($Version
|
|||||||
# --- Configuration.xml ---
|
# --- Configuration.xml ---
|
||||||
$cfgXml = @"
|
$cfgXml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$FormatVersion">
|
||||||
<Configuration uuid="$uuidCfg">
|
<Configuration uuid="$uuidCfg">
|
||||||
<InternalInfo>
|
<InternalInfo>
|
||||||
<xr:ContainedObject>
|
<xr:ContainedObject>
|
||||||
@@ -175,7 +180,7 @@ $cfgXml = @"
|
|||||||
# --- Languages/Русский.xml ---
|
# --- Languages/Русский.xml ---
|
||||||
$langXml = @"
|
$langXml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$FormatVersion">
|
||||||
<Language uuid="$uuidLang">
|
<Language uuid="$uuidLang">
|
||||||
<Properties>
|
<Properties>
|
||||||
<Name>Русский</Name>
|
<Name>Русский</Name>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cf-init v1.2 — Create empty 1C configuration scaffold
|
# cf-init v1.4 — Create empty 1C configuration scaffold
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
"""Generates minimal XML source files for a 1C configuration."""
|
"""Generates minimal XML source files for a 1C configuration."""
|
||||||
import sys, os, argparse, uuid
|
import sys, os, argparse, uuid
|
||||||
@@ -24,6 +24,11 @@ def main():
|
|||||||
parser.add_argument('-Version', dest='Version', default='')
|
parser.add_argument('-Version', dest='Version', default='')
|
||||||
parser.add_argument('-Vendor', dest='Vendor', default='')
|
parser.add_argument('-Vendor', dest='Vendor', default='')
|
||||||
parser.add_argument('-CompatibilityMode', dest='CompatibilityMode', default='Version8_3_24')
|
parser.add_argument('-CompatibilityMode', dest='CompatibilityMode', default='Version8_3_24')
|
||||||
|
# Версия формата выгрузки (MDClasses) — её задаёт ПЛАТФОРМА, а не режим совместимости:
|
||||||
|
# 8.3.20-8.3.24 пишут 2.17, 8.3.25 — 2.18, 8.3.26 — 2.19, 8.3.27 — 2.20.
|
||||||
|
# Дефолт консервативный: 2.17 читается всеми платформами.
|
||||||
|
parser.add_argument('-FormatVersion', dest='FormatVersion', default='2.17',
|
||||||
|
choices=['2.17', '2.18', '2.19', '2.20', '2.21'])
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
name = args.Name
|
name = args.Name
|
||||||
@@ -96,7 +101,7 @@ def main():
|
|||||||
\t\t\t</xr:ContainedObject>\n"""
|
\t\t\t</xr:ContainedObject>\n"""
|
||||||
|
|
||||||
cfg_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
cfg_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{args.FormatVersion}">
|
||||||
\t<Configuration uuid="{uuid_cfg}">
|
\t<Configuration uuid="{uuid_cfg}">
|
||||||
\t\t<InternalInfo>
|
\t\t<InternalInfo>
|
||||||
{contained_objects}\t\t</InternalInfo>
|
{contained_objects}\t\t</InternalInfo>
|
||||||
@@ -168,7 +173,7 @@ def main():
|
|||||||
|
|
||||||
# --- Languages/Русский.xml ---
|
# --- Languages/Русский.xml ---
|
||||||
lang_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
lang_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{args.FormatVersion}">
|
||||||
\t<Language uuid="{uuid_lang}">
|
\t<Language uuid="{uuid_lang}">
|
||||||
\t\t<Properties>
|
\t\t<Properties>
|
||||||
\t\t\t<Name>Русский</Name>
|
\t\t\t<Name>Русский</Name>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# cf-validate v1.3 — Validate 1C configuration root structure
|
# cf-validate v1.5 — Validate 1C configuration root structure
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -104,11 +104,11 @@ $validClassIds = @(
|
|||||||
"fb282519-d103-4dd3-bc12-cb271d631dfc" # home page / client app interface
|
"fb282519-d103-4dd3-bc12-cb271d631dfc" # home page / client app interface
|
||||||
)
|
)
|
||||||
|
|
||||||
# 44 types in canonical order
|
# 45 types in canonical order
|
||||||
$childObjectTypes = @(
|
$childObjectTypes = @(
|
||||||
"Language","Subsystem","StyleItem","Style",
|
"Language","Subsystem","StyleItem","Style",
|
||||||
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
||||||
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
|
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
|
||||||
"XDTOPackage","WebService","HTTPService","WSReference",
|
"XDTOPackage","WebService","HTTPService","WSReference",
|
||||||
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
||||||
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
||||||
@@ -125,6 +125,7 @@ $childTypeDirMap = @{
|
|||||||
"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"
|
||||||
"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"
|
||||||
@@ -204,8 +205,9 @@ if ($root.NamespaceURI -ne $expectedNs) {
|
|||||||
$version = $root.GetAttribute("version")
|
$version = $root.GetAttribute("version")
|
||||||
if (-not $version) {
|
if (-not $version) {
|
||||||
Report-Warn "1. Missing version attribute on MetaDataObject"
|
Report-Warn "1. Missing version attribute on MetaDataObject"
|
||||||
} elseif ($version -ne "2.17" -and $version -ne "2.20" -and $version -ne "2.21") {
|
} elseif ($version -notin @("2.17", "2.18", "2.19", "2.20", "2.21")) {
|
||||||
Report-Warn "1. Unusual version '$version' (expected 2.17, 2.20 or 2.21)"
|
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27).
|
||||||
|
Report-Warn "1. Unusual version '$version' (expected 2.17-2.20 or 2.21)"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Must have Configuration child
|
# Must have Configuration child
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cf-validate v1.3 — Validate 1C configuration XML structure
|
# cf-validate v1.5 — 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
|
||||||
@@ -33,11 +33,11 @@ VALID_CLASS_IDS = [
|
|||||||
'fb282519-d103-4dd3-bc12-cb271d631dfc', # home page / client app interface
|
'fb282519-d103-4dd3-bc12-cb271d631dfc', # home page / client app interface
|
||||||
]
|
]
|
||||||
|
|
||||||
# 44 types in canonical order
|
# 45 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', 'CommonAttribute', 'ExchangePlan',
|
'FilterCriterion', 'CommonModule', 'Bot', 'CommonAttribute', 'ExchangePlan',
|
||||||
'XDTOPackage', 'WebService', 'HTTPService', 'WSReference',
|
'XDTOPackage', 'WebService', 'HTTPService', 'WSReference',
|
||||||
'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption',
|
'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption',
|
||||||
'FunctionalOptionsParameter', 'DefinedType', 'CommonCommand', 'CommandGroup',
|
'FunctionalOptionsParameter', 'DefinedType', 'CommonCommand', 'CommandGroup',
|
||||||
@@ -54,6 +54,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',
|
||||||
'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',
|
||||||
@@ -231,8 +232,9 @@ def main():
|
|||||||
version = root.get('version', '')
|
version = root.get('version', '')
|
||||||
if not version:
|
if not version:
|
||||||
r.warn('1. Missing version attribute on MetaDataObject')
|
r.warn('1. Missing version attribute on MetaDataObject')
|
||||||
elif version not in ('2.17', '2.20', '2.21'):
|
elif version not in ('2.17', '2.18', '2.19', '2.20', '2.21'):
|
||||||
r.warn(f"1. Unusual version '{version}' (expected 2.17, 2.20 or 2.21)")
|
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27).
|
||||||
|
r.warn(f"1. Unusual version '{version}' (expected 2.17-2.20 or 2.21)")
|
||||||
|
|
||||||
# Must have Configuration child
|
# Must have Configuration child
|
||||||
cfg_node = None
|
cfg_node = None
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# cfe-borrow v1.8 — Borrow objects from configuration into extension (CFE)
|
# cfe-borrow v1.11 — Borrow objects from configuration into extension (CFE)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)][string]$ExtensionPath,
|
[Parameter(Mandatory)][string]$ExtensionPath,
|
||||||
@@ -285,14 +285,36 @@ $script:generatedTypes = @{
|
|||||||
"DefinedType" = @(
|
"DefinedType" = @(
|
||||||
@{ prefix = "DefinedType"; category = "DefinedType" }
|
@{ prefix = "DefinedType"; category = "DefinedType" }
|
||||||
)
|
)
|
||||||
|
"Sequence" = @(
|
||||||
|
@{ prefix = "SequenceRecord"; category = "Record" }
|
||||||
|
@{ prefix = "SequenceManager"; category = "Manager" }
|
||||||
|
@{ prefix = "SequenceRecordSet"; category = "RecordSet" }
|
||||||
|
)
|
||||||
|
"FilterCriterion" = @(
|
||||||
|
@{ prefix = "FilterCriterionManager"; category = "Manager" }
|
||||||
|
@{ prefix = "FilterCriterionList"; category = "List" }
|
||||||
|
)
|
||||||
|
"SettingsStorage" = @(
|
||||||
|
@{ prefix = "SettingsStorageManager"; category = "Manager" }
|
||||||
|
)
|
||||||
|
"IntegrationService" = @(
|
||||||
|
@{ prefix = "IntegrationServiceManager"; category = "Manager" }
|
||||||
|
)
|
||||||
|
"WSReference" = @(
|
||||||
|
@{ prefix = "WSReferenceManager"; category = "Manager" }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
# Types that need ChildObjects element
|
# Types that need ChildObjects element — fallback when the source object cannot be probed.
|
||||||
|
# The platform emits <ChildObjects> for every container type even when empty, and rejects
|
||||||
|
# the file without it ("ожидаемое ChildObjects"); primary signal is the source object itself.
|
||||||
$typesWithChildObjects = @(
|
$typesWithChildObjects = @(
|
||||||
"Catalog","Document","ExchangePlan","ChartOfAccounts",
|
"Catalog","Document","ExchangePlan","ChartOfAccounts",
|
||||||
"ChartOfCharacteristicTypes","ChartOfCalculationTypes",
|
"ChartOfCharacteristicTypes","ChartOfCalculationTypes",
|
||||||
"BusinessProcess","Task","Enum",
|
"BusinessProcess","Task","Enum",
|
||||||
"InformationRegister","AccumulationRegister","AccountingRegister","CalculationRegister"
|
"InformationRegister","AccumulationRegister","AccountingRegister","CalculationRegister",
|
||||||
|
"DataProcessor","Report","DocumentJournal","FilterCriterion","SettingsStorage",
|
||||||
|
"Sequence","HTTPService","WebService","IntegrationService","Subsystem"
|
||||||
)
|
)
|
||||||
|
|
||||||
# CommonModule properties to copy from source
|
# CommonModule properties to copy from source
|
||||||
@@ -348,7 +370,10 @@ function Detect-FormatVersion([string]$dir) {
|
|||||||
while ($d) {
|
while ($d) {
|
||||||
$cfgPath = Join-Path $d "Configuration.xml"
|
$cfgPath = Join-Path $d "Configuration.xml"
|
||||||
if (Test-Path $cfgPath) {
|
if (Test-Path $cfgPath) {
|
||||||
$head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length))
|
$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] }
|
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
||||||
}
|
}
|
||||||
$parent = Split-Path $d -Parent
|
$parent = Split-Path $d -Parent
|
||||||
@@ -454,6 +479,9 @@ function Read-SourceObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Whether the platform emits <ChildObjects> for this type — the source object is the ground truth
|
||||||
|
$srcProps["__HasChildObjects"] = ($srcEl.SelectSingleNode("md:ChildObjects", $srcNs) -ne $null)
|
||||||
|
|
||||||
return @{
|
return @{
|
||||||
Uuid = $srcUuid
|
Uuid = $srcUuid
|
||||||
Properties = $srcProps
|
Properties = $srcProps
|
||||||
@@ -1669,7 +1697,7 @@ function Build-BorrowedObjectXml {
|
|||||||
$sb.AppendLine("`t`t</Properties>") | Out-Null
|
$sb.AppendLine("`t`t</Properties>") | Out-Null
|
||||||
|
|
||||||
# ChildObjects (for types that need it)
|
# ChildObjects (for types that need it)
|
||||||
if ($typesWithChildObjects -contains $typeName) {
|
if ($sourceProps["__HasChildObjects"] -or ($typesWithChildObjects -contains $typeName)) {
|
||||||
$sb.AppendLine("`t`t<ChildObjects/>") | Out-Null
|
$sb.AppendLine("`t`t<ChildObjects/>") | Out-Null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cfe-borrow v1.8 — Borrow objects from configuration into extension (CFE)
|
# cfe-borrow v1.11 — 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
|
||||||
@@ -254,13 +254,36 @@ GENERATED_TYPES = {
|
|||||||
"DefinedType": [
|
"DefinedType": [
|
||||||
{"prefix": "DefinedType", "category": "DefinedType"},
|
{"prefix": "DefinedType", "category": "DefinedType"},
|
||||||
],
|
],
|
||||||
|
"Sequence": [
|
||||||
|
{"prefix": "SequenceRecord", "category": "Record"},
|
||||||
|
{"prefix": "SequenceManager", "category": "Manager"},
|
||||||
|
{"prefix": "SequenceRecordSet", "category": "RecordSet"},
|
||||||
|
],
|
||||||
|
"FilterCriterion": [
|
||||||
|
{"prefix": "FilterCriterionManager", "category": "Manager"},
|
||||||
|
{"prefix": "FilterCriterionList", "category": "List"},
|
||||||
|
],
|
||||||
|
"SettingsStorage": [
|
||||||
|
{"prefix": "SettingsStorageManager", "category": "Manager"},
|
||||||
|
],
|
||||||
|
"IntegrationService": [
|
||||||
|
{"prefix": "IntegrationServiceManager", "category": "Manager"},
|
||||||
|
],
|
||||||
|
"WSReference": [
|
||||||
|
{"prefix": "WSReferenceManager", "category": "Manager"},
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Types that need ChildObjects element — fallback when the source object cannot be probed.
|
||||||
|
# The platform emits <ChildObjects> for every container type even when empty, and rejects
|
||||||
|
# the file without it ("expected ChildObjects"); primary signal is the source object itself.
|
||||||
TYPES_WITH_CHILD_OBJECTS = [
|
TYPES_WITH_CHILD_OBJECTS = [
|
||||||
"Catalog", "Document", "ExchangePlan", "ChartOfAccounts",
|
"Catalog", "Document", "ExchangePlan", "ChartOfAccounts",
|
||||||
"ChartOfCharacteristicTypes", "ChartOfCalculationTypes",
|
"ChartOfCharacteristicTypes", "ChartOfCalculationTypes",
|
||||||
"BusinessProcess", "Task", "Enum",
|
"BusinessProcess", "Task", "Enum",
|
||||||
"InformationRegister", "AccumulationRegister", "AccountingRegister", "CalculationRegister",
|
"InformationRegister", "AccumulationRegister", "AccountingRegister", "CalculationRegister",
|
||||||
|
"DataProcessor", "Report", "DocumentJournal", "FilterCriterion", "SettingsStorage",
|
||||||
|
"Sequence", "HTTPService", "WebService", "IntegrationService", "Subsystem",
|
||||||
]
|
]
|
||||||
|
|
||||||
COMMON_MODULE_PROPS = ["Global", "ClientManagedApplication", "Server", "ExternalConnection", "ClientOrdinaryApplication", "ServerCall"]
|
COMMON_MODULE_PROPS = ["Global", "ClientManagedApplication", "Server", "ExternalConnection", "ClientOrdinaryApplication", "ServerCall"]
|
||||||
@@ -349,13 +372,49 @@ def expand_self_closing(container, parent_indent):
|
|||||||
container.text = "\r\n" + parent_indent
|
container.text = "\r\n" + parent_indent
|
||||||
|
|
||||||
|
|
||||||
def save_xml_bom(tree, path):
|
def _detect_xml_style(path):
|
||||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
"""Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
|
||||||
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
финальный перенос. None → файл новый (сохранить текущее поведение)."""
|
||||||
if not xml_bytes.endswith(b"\n"):
|
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)."""
|
||||||
|
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"))
|
||||||
|
# Финальный перенос — как в оригинале (новый файл → есть)
|
||||||
|
want_final_nl = style["final_nl"] if style else True
|
||||||
|
xml_bytes = xml_bytes.rstrip(b"\n")
|
||||||
|
if want_final_nl:
|
||||||
xml_bytes += b"\n"
|
xml_bytes += b"\n"
|
||||||
|
# EOL — как в оригинале (новый файл → LF, текущее поведение)
|
||||||
|
if style and style["crlf"]:
|
||||||
|
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
|
||||||
|
return xml_bytes
|
||||||
|
|
||||||
|
|
||||||
|
def save_xml_bom(tree, path):
|
||||||
|
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:
|
with open(path, "wb") as f:
|
||||||
f.write(b"\xef\xbb\xbf")
|
if style is None or style["bom"]:
|
||||||
|
f.write(b"\xef\xbb\xbf")
|
||||||
f.write(xml_bytes)
|
f.write(xml_bytes)
|
||||||
|
|
||||||
|
|
||||||
@@ -500,6 +559,9 @@ def main():
|
|||||||
type_xml = etree.tostring(type_node, encoding="unicode")
|
type_xml = etree.tostring(type_node, encoding="unicode")
|
||||||
src_props["__TypeXml"] = re.sub(r'\s+xmlns(?::\w+)?="[^"]*"', '', type_xml)
|
src_props["__TypeXml"] = re.sub(r'\s+xmlns(?::\w+)?="[^"]*"', '', type_xml)
|
||||||
|
|
||||||
|
# Whether the platform emits <ChildObjects> for this type — the source object is the ground truth
|
||||||
|
src_props["__HasChildObjects"] = src_el.find(f"{{{MD_NS}}}ChildObjects") is not None
|
||||||
|
|
||||||
return {"Uuid": src_uuid, "Properties": src_props, "Element": src_el}
|
return {"Uuid": src_uuid, "Properties": src_props, "Element": src_el}
|
||||||
|
|
||||||
def read_source_form_uuid(type_name, obj_name, form_name):
|
def read_source_form_uuid(type_name, obj_name, form_name):
|
||||||
@@ -576,7 +638,7 @@ def main():
|
|||||||
|
|
||||||
lines.append("\t\t</Properties>")
|
lines.append("\t\t</Properties>")
|
||||||
|
|
||||||
if type_name in TYPES_WITH_CHILD_OBJECTS:
|
if source_props.get("__HasChildObjects") or type_name in TYPES_WITH_CHILD_OBJECTS:
|
||||||
lines.append("\t\t<ChildObjects/>")
|
lines.append("\t\t<ChildObjects/>")
|
||||||
|
|
||||||
lines.append(f"\t</{type_name}>")
|
lines.append(f"\t</{type_name}>")
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: cfe-patch-method
|
name: cfe-patch-method
|
||||||
description: Генерация перехватчика метода в расширении 1С (CFE). Используй когда нужно перехватить метод заимствованного объекта — вставить код до, после или вместо оригинального
|
description: Генерация и актуализация перехватчика метода в расширении 1С (CFE). Используй когда нужно перехватить метод заимствованного объекта — вставить код до, после, вместо оригинала, изменить его тело (ИзменениеИКонтроль) — или актуализировать перехватчик после изменения оригинала
|
||||||
argument-hint: -ExtensionPath <path> -ModulePath "Catalog.X.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
argument-hint: -ExtensionPath <path> -ConfigPath <path> -ModulePath "Catalog.X.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash
|
- Bash
|
||||||
- Read
|
- Read
|
||||||
@@ -10,22 +10,31 @@ allowed-tools:
|
|||||||
|
|
||||||
# /cfe-patch-method — Генерация перехватчика метода
|
# /cfe-patch-method — Генерация перехватчика метода
|
||||||
|
|
||||||
Генерирует `.bsl` файл с декоратором перехвата для заимствованного объекта расширения. Создаёт файл или дописывает в существующий.
|
Читает оригинал метода из конфигурации-источника и генерирует `.bsl`-перехватчик заимствованного объекта: с правильной директивой контекста, полной сигнатурой, обрамляющими инструкциями препроцессора и областями. Для `ИзменениеИКонтроль` копирует всё тело оригинала. Создаёт файл модуля, дописывает в существующий или актуализирует уже заимствованный метод.
|
||||||
|
|
||||||
## Предусловие
|
## Предусловие
|
||||||
|
|
||||||
Объект должен быть заимствован в расширение (`/cfe-borrow`). Скрипт читает `NamePrefix` из `Configuration.xml` расширения для формирования имени процедуры.
|
Объект должен быть заимствован в расширение (`/cfe-borrow`). Нужен доступ к исходникам базовой конфигурации (`-ConfigPath`) — оттуда читается оригинал метода. `NamePrefix` берётся из `Configuration.xml` расширения.
|
||||||
|
|
||||||
|
### Авто-определение ConfigPath
|
||||||
|
|
||||||
|
Если пользователь не указал `-ConfigPath` — попробуй определить автоматически:
|
||||||
|
1. Прочитай `.v8-project.json` из корня проекта
|
||||||
|
2. Разреши целевую базу (по имени, ветке или `default` — алгоритм из `/db-list`)
|
||||||
|
3. Если у базы есть поле `configSrc` — используй как `-ConfigPath`
|
||||||
|
4. Если `configSrc` нет — спроси у пользователя
|
||||||
|
|
||||||
## Параметры
|
## Параметры
|
||||||
|
|
||||||
| Параметр | Описание | По умолчанию |
|
| Параметр | Описание | По умолчанию |
|
||||||
|----------|----------|--------------|
|
|----------|----------|--------------|
|
||||||
| `ExtensionPath` | Путь к расширению (обязат.) | — |
|
| `ExtensionPath` | Путь к расширению (обязат.) | — |
|
||||||
| `ModulePath` | Путь к модулю (обязат.) | — |
|
| `ConfigPath` | Путь к конфигурации-источнику | обязат., кроме случая, когда `ModulePath` — путь к файлу |
|
||||||
| `MethodName` | Имя перехватываемого метода (обязат.) | — |
|
| `ModulePath` | Логическое имя (`Тип.Имя.Модуль`) **или** путь к файлу модуля `.bsl` | обязат. для генерации |
|
||||||
| `InterceptorType` | `Before` / `After` / `ModificationAndControl` (обязат.) | — |
|
| `MethodName` | Имя перехватываемого метода | обязат. для генерации |
|
||||||
| `Context` | Директива контекста | `НаСервере` |
|
| `InterceptorType` | `Before` / `After` / `Instead` / `ModificationAndControl` | обязат. для генерации |
|
||||||
| `IsFunction` | Метод — функция (добавит `Возврат`) | false |
|
| `Check` | Проверить контролируемые методы на дрейф (только отчёт) | — |
|
||||||
|
| `Actualize` | Актуализировать дрейфнувшие контролируемые методы | — |
|
||||||
|
|
||||||
## Формат ModulePath
|
## Формат ModulePath
|
||||||
|
|
||||||
@@ -40,39 +49,97 @@ allowed-tools:
|
|||||||
|
|
||||||
Аналогично для Report, DataProcessor, InformationRegister и других типов.
|
Аналогично для Report, DataProcessor, InformationRegister и других типов.
|
||||||
|
|
||||||
|
Вместо логического имени в `ModulePath` можно передать **путь к файлу** модуля-источника `.bsl` — тогда `-ConfigPath` указывать не нужно (оригинал читается прямо из этого файла). Путь модуля расширения определяется от типовой папки в пути автоматически.
|
||||||
|
|
||||||
## Типы перехвата
|
## Типы перехвата
|
||||||
|
|
||||||
| InterceptorType | Декоратор | Назначение |
|
| InterceptorType | Декоратор | Назначение | Применим к |
|
||||||
|-----------------|-----------|------------|
|
|-----------------|-----------|------------|------------|
|
||||||
| `Before` | `&Перед` | Код до вызова оригинального метода |
|
| `Before` | `&Перед` | Код до вызова оригинального метода | процедуры |
|
||||||
| `After` | `&После` | Код после вызова оригинального метода |
|
| `After` | `&После` | Код после вызова оригинального метода | процедуры |
|
||||||
| `ModificationAndControl` | `&ИзменениеИКонтроль` | Копия тела метода с маркерами `#Вставка`/`#Удаление` |
|
| `Instead` | `&Вместо` | Замена метода; в теле — скаффолд `ПродолжитьВызов(...)` | процедуры и функции |
|
||||||
|
| `ModificationAndControl` | `&ИзменениеИКонтроль` | Копия тела оригинала для правки маркерами `#Вставка`/`#Удаление` | процедуры и функции |
|
||||||
|
|
||||||
|
## Маркеры `#Вставка` / `#Удаление` (для `ModificationAndControl`)
|
||||||
|
|
||||||
|
`&ИзменениеИКонтроль` вставляет в перехватчик **копию тела оригинала**. Дальше отредактируй тело под свою доработку, **помечая каждое изменение** — платформа так отличает твою правку от неизменного оригинала:
|
||||||
|
|
||||||
|
- **Добавляешь код** → оберни его `#Вставка` … `#КонецВставки`.
|
||||||
|
- **Удаляешь код оригинала** → оберни удаляемые строки `#Удаление` … `#КонецУдаления`, но сами строки **оставь между маркерами** (платформа сверяет их с оригиналом).
|
||||||
|
- **Заменяешь** → `#Удаление` старое `#КонецУдаления` сразу за ним `#Вставка` новое `#КонецВставки`.
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
```bsl
|
||||||
|
&ИзменениеИКонтроль("ПриЗаписи")
|
||||||
|
Процедура Расш_ПриЗаписи(Отказ)
|
||||||
|
СуммаДокумента = РассчитатьСумму();
|
||||||
|
#Вставка
|
||||||
|
// доработка: округляем
|
||||||
|
СуммаДокумента = Окр(СуммаДокумента, 2);
|
||||||
|
#КонецВставки
|
||||||
|
#Удаление
|
||||||
|
Записать();
|
||||||
|
#КонецУдаления
|
||||||
|
#Вставка
|
||||||
|
ЗаписатьСПроверкой(Отказ);
|
||||||
|
#КонецВставки
|
||||||
|
КонецПроцедуры
|
||||||
|
```
|
||||||
|
|
||||||
|
Правила:
|
||||||
|
- Маркеры — на **отдельной строке с 0-й колонки** (без отступа), даже внутри отступов и текста запроса (`|…`).
|
||||||
|
- **Незамеченные (unmarked) строки должны совпадать с оригиналом дословно** — это и есть «контроль». Если оригинал в конфигурации-источнике изменится, unmarked-контекст разойдётся → метод потребует актуализации (см. ниже; проверить пачкой — `-Check`).
|
||||||
|
- Меняешь только свои `#Вставка`/`#Удаление`; чужой оригинал не трогай.
|
||||||
|
|
||||||
|
## Актуализация
|
||||||
|
|
||||||
|
После изменения оригинала в конфигурации-источнике перехватчик `&ИзменениеИКонтроль` может рассинхронизироваться — unmarked-контекст разойдётся с новым оригиналом. Платформа при загрузке об этом молчит, поэтому актуализируй сам:
|
||||||
|
|
||||||
|
- **Проверить** — `-Check`: отчёт по всем контролируемым методам расширения, ничего не пишет (`exit 1`, если есть дрейф или конфликт).
|
||||||
|
- **Актуализировать** — `-Actualize`: переносит правки (`#Вставка`/`#Удаление`) на новый оригинал по всему расширению. Сузить область: `-ModulePath` — один модуль, `+ -MethodName` — один метод. Для одного метода то же делает повторный вызов с `-InterceptorType ModificationAndControl`.
|
||||||
|
|
||||||
|
Статусы в выводе:
|
||||||
|
|
||||||
|
- `[АКТУАЛЕН]` — оригинал не менялся, правок нет;
|
||||||
|
- `[АКТУАЛИЗИРОВАН]` — тело обновлено по новому оригиналу, правки сохранены (в выводе — сводка);
|
||||||
|
- `[АКТУАЛИЗИРОВАН-ЧАСТИЧНО]` — часть правок не удалось разместить (якорь изменился в новом оригинале). Они помечены `// [РЕСИНК-КОНФЛИКТ]` в модуле и не потеряны; путь к merge-воркспейсу — в выводе (начни с `index.md`, дальше по каждому конфликту его `conflict.md`, размести блоки вручную в `.bsl` расширения);
|
||||||
|
- `[ПЕРЕНЕСЕНО В ОСНОВНУЮ]` — правка уже есть в новом оригинале (вставленный код внесён вендором) или удаляемый блок уже вырезан. Правка неактуальна — убирается из тела, дублировать/конфликтовать не нужно. Если так со всеми правками метода — перехватчик можно удалить. `-Check` этим не роняет `exit`.
|
||||||
|
|
||||||
|
Повторный вызов `Before`/`After`/`Instead` для уже перехваченного метода дубль не создаёт (`[ПРОПУЩЕН]`).
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-patch-method.ps1" -ExtensionPath src -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-patch-method.ps1" -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
||||||
```
|
```
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Перехват &Перед на сервере
|
# Код перед записью
|
||||||
... -ExtensionPath src -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
||||||
|
|
||||||
# Перехват &После на клиенте
|
# Перехват После на форме
|
||||||
... -ExtensionPath src -ModulePath "Document.Заказ.Form.ФормаДокумента" -MethodName "ПослеЗаписиНаСервере" -InterceptorType After -Context "НаКлиенте"
|
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "Document.Заказ.Form.ФормаДокумента" -MethodName "ПослеЗаписиНаСервере" -InterceptorType After
|
||||||
|
|
||||||
# ИзменениеИКонтроль для функции
|
# Замена функции (ПродолжитьВызов)
|
||||||
... -ExtensionPath src -ModulePath "CommonModule.ОбщийМодуль" -MethodName "ПолучитьДанные" -InterceptorType ModificationAndControl -IsFunction
|
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "CommonModule.ОбщийМодуль" -MethodName "ПолучитьДанные" -InterceptorType Instead
|
||||||
|
|
||||||
|
# ИзменениеИКонтроль — копия тела для правки маркерами
|
||||||
|
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "Document.РеализацияТоваров.ObjectModule" -MethodName "ОбработкаПроведения" -InterceptorType ModificationAndControl
|
||||||
|
|
||||||
|
# ModulePath как путь к файлу модуля-источника (без -ConfigPath)
|
||||||
|
... -ExtensionPath src\cfe\ИмяРасширения -ModulePath "src\cf\CommonModules\ОбщийМодуль\Ext\Module.bsl" -MethodName "ПолучитьДанные" -InterceptorType Instead
|
||||||
|
|
||||||
|
# Проверить все контролируемые методы расширения на дрейф
|
||||||
|
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -Check
|
||||||
|
|
||||||
|
# Актуализировать дрейфнувшие контролируемые методы пачкой
|
||||||
|
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -Actualize
|
||||||
```
|
```
|
||||||
|
|
||||||
## Генерируемый код (Before)
|
## Верификация
|
||||||
|
|
||||||
```bsl
|
```
|
||||||
&НаСервере
|
/cfe-validate <ExtensionPath>
|
||||||
&Перед("ПриЗаписи")
|
|
||||||
Процедура Расш1_ПриЗаписи()
|
|
||||||
// TODO: код перед вызовом оригинального метода
|
|
||||||
КонецПроцедуры
|
|
||||||
```
|
```
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
# cfe-validate v1.4 — Validate 1C configuration extension structure (CFE)
|
# cfe-validate v1.5 — Validate 1C configuration extension structure (CFE)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -197,8 +197,9 @@ if ($root.NamespaceURI -ne $expectedNs) {
|
|||||||
$version = $root.GetAttribute("version")
|
$version = $root.GetAttribute("version")
|
||||||
if (-not $version) {
|
if (-not $version) {
|
||||||
Report-Warn "1. Missing version attribute on MetaDataObject"
|
Report-Warn "1. Missing version attribute on MetaDataObject"
|
||||||
} elseif ($version -ne "2.17" -and $version -ne "2.20" -and $version -ne "2.21") {
|
} elseif ($version -notin @("2.17", "2.18", "2.19", "2.20", "2.21")) {
|
||||||
Report-Warn "1. Unusual version '$version' (expected 2.17, 2.20 or 2.21)"
|
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27).
|
||||||
|
Report-Warn "1. Unusual version '$version' (expected 2.17-2.20 or 2.21)"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Must have Configuration child
|
# Must have Configuration child
|
||||||
@@ -930,6 +931,17 @@ if ($script:borrowedFormsWithTree.Count -eq 0) {
|
|||||||
Report-OK "13. TypeLink: clean"
|
Report-OK "13. TypeLink: clean"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Breadcrumb: controlled methods (&ИзменениеИКонтроль) drift is not checked here ---
|
||||||
|
$extRootDir = Split-Path $resolvedPath -Parent
|
||||||
|
$ctrlCount = 0
|
||||||
|
foreach ($bslFile in (Get-ChildItem -Path $extRootDir -Recurse -Filter *.bsl -File -ErrorAction SilentlyContinue)) {
|
||||||
|
$txt = [System.IO.File]::ReadAllText($bslFile.FullName, [System.Text.Encoding]::UTF8)
|
||||||
|
$ctrlCount += ([regex]::Matches($txt, '(?m)^\s*&ИзменениеИКонтроль\(')).Count
|
||||||
|
}
|
||||||
|
if ($ctrlCount -gt 0) {
|
||||||
|
Out-Line "[INFO] Контролируемых методов (&ИзменениеИКонтроль): $ctrlCount — их актуальность здесь не проверяется. Сверьте: /cfe-patch-method -Check -ExtensionPath <ext> -ConfigPath <cf>"
|
||||||
|
}
|
||||||
|
|
||||||
# --- Final output ---
|
# --- Final output ---
|
||||||
& $finalize
|
& $finalize
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cfe-validate v1.4 — Validate 1C configuration extension XML structure (CFE)
|
# cfe-validate v1.5 — 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
|
||||||
@@ -216,8 +216,9 @@ def main():
|
|||||||
version = root.get('version', '')
|
version = root.get('version', '')
|
||||||
if not version:
|
if not version:
|
||||||
r.warn('1. Missing version attribute on MetaDataObject')
|
r.warn('1. Missing version attribute on MetaDataObject')
|
||||||
elif version not in ('2.17', '2.20', '2.21'):
|
elif version not in ('2.17', '2.18', '2.19', '2.20', '2.21'):
|
||||||
r.warn(f"1. Unusual version '{version}' (expected 2.17, 2.20 or 2.21)")
|
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27).
|
||||||
|
r.warn(f"1. Unusual version '{version}' (expected 2.17-2.20 or 2.21)")
|
||||||
|
|
||||||
# Must have Configuration child
|
# Must have Configuration child
|
||||||
cfg_node = None
|
cfg_node = None
|
||||||
@@ -885,6 +886,21 @@ def main():
|
|||||||
elif check13_ok:
|
elif check13_ok:
|
||||||
r.ok('13. TypeLink: clean')
|
r.ok('13. TypeLink: clean')
|
||||||
|
|
||||||
|
# --- Breadcrumb: controlled methods (&ИзменениеИКонтроль) drift is not checked here ---
|
||||||
|
ctrl_count = 0
|
||||||
|
for dp, _dn, files in os.walk(config_dir):
|
||||||
|
for fn in files:
|
||||||
|
if fn.endswith('.bsl'):
|
||||||
|
try:
|
||||||
|
with open(os.path.join(dp, fn), 'r', encoding='utf-8-sig') as f:
|
||||||
|
for ln in f:
|
||||||
|
if re.match(r'^\s*&ИзменениеИКонтроль\(', ln):
|
||||||
|
ctrl_count += 1
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
if ctrl_count > 0:
|
||||||
|
r.out('[INFO] Контролируемых методов (&ИзменениеИКонтроль): %d — их актуальность здесь не проверяется. Сверьте: /cfe-patch-method -Check -ExtensionPath <ext> -ConfigPath <cf>' % ctrl_count)
|
||||||
|
|
||||||
# --- Final output ---
|
# --- Final output ---
|
||||||
r.finalize(out_file)
|
r.finalize(out_file)
|
||||||
sys.exit(1 if r.errors > 0 else 0)
|
sys.exit(1 if r.errors > 0 else 0)
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" <п
|
|||||||
| `-UseTemplate <файл>` | нет | Создать из шаблона (.cf или .dt) |
|
| `-UseTemplate <файл>` | нет | Создать из шаблона (.cf или .dt) |
|
||||||
| `-AddToList` | нет | Добавить в список баз 1С |
|
| `-AddToList` | нет | Добавить в список баз 1С |
|
||||||
| `-ListName <имя>` | нет | Имя базы в списке |
|
| `-ListName <имя>` | нет | Имя базы в списке |
|
||||||
|
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
|
||||||
|
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
|
||||||
|
|
||||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# db-create v1.4 — Create 1C information base
|
# db-create v1.10 — Create 1C information base
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Создание информационной базы 1С
|
Создание информационной базы 1С
|
||||||
@@ -29,6 +30,12 @@
|
|||||||
.PARAMETER ListName
|
.PARAMETER ListName
|
||||||
Имя базы в списке
|
Имя базы в списке
|
||||||
|
|
||||||
|
.PARAMETER AdditionalV8Arguments
|
||||||
|
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
|
||||||
|
|
||||||
|
.PARAMETER AdditionalIbcmdArguments
|
||||||
|
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
|
||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
.\db-create.ps1 -InfoBasePath "C:\Bases\NewDB"
|
.\db-create.ps1 -InfoBasePath "C:\Bases\NewDB"
|
||||||
|
|
||||||
@@ -60,12 +67,163 @@ param(
|
|||||||
[switch]$AddToList,
|
[switch]$AddToList,
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[string]$ListName
|
[string]$ListName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalV8Arguments = @(),
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalIbcmdArguments = @()
|
||||||
)
|
)
|
||||||
|
|
||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
$script:V8OwnedKeys = @(
|
||||||
|
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
|
||||||
|
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
|
||||||
|
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
|
||||||
|
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
|
||||||
|
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||||
|
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||||
|
)
|
||||||
|
$script:IbcmdOwnedKeys = @(
|
||||||
|
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||||
|
'--import', '--export', '--apply', '--force', '--create-database',
|
||||||
|
'--user', '--password'
|
||||||
|
)
|
||||||
|
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
|
||||||
|
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
|
||||||
|
|
||||||
|
function Test-ArgKeyMatch {
|
||||||
|
# A token matches a key when it equals the key, or starts with it and the next
|
||||||
|
# character is not a letter — catches glued /N"user" and --password=x, while
|
||||||
|
# keeping /ClearCache distinct from /C.
|
||||||
|
param([string]$Token, [string]$Key)
|
||||||
|
if ($Token.Length -lt $Key.Length) { return $false }
|
||||||
|
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
|
||||||
|
if ($Token.Length -eq $Key.Length) { return $true }
|
||||||
|
return -not [char]::IsLetter($Token[$Key.Length])
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ProjectExtraArgs {
|
||||||
|
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
|
||||||
|
param([string]$Name)
|
||||||
|
$dir = (Get-Location).Path
|
||||||
|
while ($dir) {
|
||||||
|
$pf = Join-Path $dir ".v8-project.json"
|
||||||
|
if (Test-Path $pf) {
|
||||||
|
try {
|
||||||
|
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
|
||||||
|
} catch {}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
$parent = Split-Path $dir -Parent
|
||||||
|
if (-not $parent -or $parent -eq $dir) { break }
|
||||||
|
$dir = $parent
|
||||||
|
}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Assert-ExtraArgs {
|
||||||
|
# The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
# output key fails with an opaque 1C error — reject what the skill owns itself.
|
||||||
|
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
|
||||||
|
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
|
||||||
|
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
|
||||||
|
foreach ($tok in $ExtraArgs) {
|
||||||
|
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
|
||||||
|
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
foreach ($k in $owned) {
|
||||||
|
if (Test-ArgKeyMatch $tok $k) {
|
||||||
|
$hint = ''
|
||||||
|
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
|
||||||
|
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-ExtraArgs {
|
||||||
|
# Pick the argument list for the selected engine and validate it. An explicitly passed
|
||||||
|
# parameter for the other engine is an error; the same keys coming from .v8-project.json
|
||||||
|
# simply do not apply — a project may describe both engines.
|
||||||
|
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
|
||||||
|
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
|
||||||
|
# space-separated values spill into positional ones, a comma-joined list arrives as a
|
||||||
|
# single token. So accept the repo's list convention (comma-separated) and split here;
|
||||||
|
# a native array call keeps working. A value containing a comma is not supported.
|
||||||
|
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -eq 'ibcmd') {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
|
||||||
|
} else {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
|
||||||
|
}
|
||||||
|
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
|
||||||
|
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
|
||||||
|
# would nest the array — the tokens would then be glued into one argument.
|
||||||
|
return $extra
|
||||||
|
}
|
||||||
|
|
||||||
|
function Format-ArgsForDisplay {
|
||||||
|
# Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
# a leaked password does.
|
||||||
|
param([string[]]$ArgList, [string]$Engine)
|
||||||
|
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
|
||||||
|
$res = @()
|
||||||
|
$maskNext = $false
|
||||||
|
foreach ($tok in $ArgList) {
|
||||||
|
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
|
||||||
|
$hit = $null
|
||||||
|
foreach ($k in $keys) {
|
||||||
|
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
|
||||||
|
}
|
||||||
|
if (-not $hit) { $res += $tok; continue }
|
||||||
|
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
|
||||||
|
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
|
||||||
|
else { $res += ($hit + '***') }
|
||||||
|
}
|
||||||
|
return ,$res
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-CleanPath {
|
||||||
|
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
# inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
|
||||||
|
param([string]$Value, [string]$ParamName)
|
||||||
|
if (-not $Value) { return $Value }
|
||||||
|
$v = $Value.Trim()
|
||||||
|
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
|
||||||
|
$v = $v.Substring(1, $v.Length - 2).Trim()
|
||||||
|
}
|
||||||
|
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
|
||||||
|
if ($v.Contains('"')) {
|
||||||
|
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
return $v
|
||||||
|
}
|
||||||
|
|
||||||
|
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
|
||||||
|
$InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath'
|
||||||
|
$UseTemplate = ConvertTo-CleanPath $UseTemplate '-UseTemplate'
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
function Find-ProjectV8Path {
|
||||||
$dir = (Get-Location).Path
|
$dir = (Get-Location).Path
|
||||||
@@ -96,7 +254,7 @@ if (-not $V8Path) {
|
|||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -105,13 +263,95 @@ if (Test-Path $V8Path -PathType Container) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
|
function ConvertFrom-PlatformBytes {
|
||||||
|
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
|
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
|
||||||
|
# one of them outright mangles Cyrillic.
|
||||||
|
param([byte[]]$Bytes)
|
||||||
|
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
|
||||||
|
try {
|
||||||
|
$strict = New-Object System.Text.UTF8Encoding($false, $true)
|
||||||
|
return $strict.GetString($Bytes)
|
||||||
|
} catch {
|
||||||
|
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-PlatformProcess {
|
||||||
|
# Run the platform non-interactively and capture its console output. A closed stdin pipe
|
||||||
|
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
|
||||||
|
# text out of our stream until we print it labelled (and out of the wrong encoding).
|
||||||
|
# Returns @{ Output; ExitCode }.
|
||||||
|
#
|
||||||
|
# Quoting differs by engine, so the caller says which it built:
|
||||||
|
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
|
||||||
|
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
|
||||||
|
# which is where 1C's own parser expects them; quoting again breaks the value.
|
||||||
|
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = if ($PreQuoted) {
|
||||||
|
$ProcArgs -join ' '
|
||||||
|
} else {
|
||||||
|
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
}
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
# stderr is drained in parallel: reading the streams one after another deadlocks
|
||||||
|
# as soon as the other one fills its pipe buffer.
|
||||||
|
$errMs = New-Object System.IO.MemoryStream
|
||||||
|
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
|
||||||
|
$outMs = New-Object System.IO.MemoryStream
|
||||||
|
$p.StandardOutput.BaseStream.CopyTo($outMs)
|
||||||
|
$errTask.Wait()
|
||||||
|
$p.WaitForExit()
|
||||||
|
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
|
||||||
|
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-PlatformOutput {
|
||||||
|
# Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
|
||||||
|
param([string]$Text)
|
||||||
|
if (-not $Text) { return }
|
||||||
|
$t = $Text.TrimEnd()
|
||||||
|
if (-not $t) { return }
|
||||||
|
$limit = 65536
|
||||||
|
if ($t.Length -gt $limit) {
|
||||||
|
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
|
||||||
|
}
|
||||||
|
Write-Host "--- Вывод платформы ---"
|
||||||
|
Write-Host $t
|
||||||
|
Write-Host "--- End ---"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function Test-FileIbCreated {
|
||||||
|
# File-infobase postcondition: the platform must have produced a non-empty 1Cv8.1CD.
|
||||||
|
# Exit code 0 without it (broken/headless env) is a false success — reject it.
|
||||||
|
param([string]$IbPath)
|
||||||
|
$f = Join-Path $IbPath "1Cv8.1CD"
|
||||||
|
return (Test-Path $f) -and ((Get-Item $f -ErrorAction SilentlyContinue).Length -gt 0)
|
||||||
|
}
|
||||||
|
|
||||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/UseTemplate' = '-UseTemplate'; '/AddToList' = '-AddToList'; '--db-path' = '-InfoBasePath'; '--load' = '-UseTemplate'; '--restore' = '-UseTemplate' }
|
||||||
|
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if ($engine -eq "ibcmd") {
|
if ($engine -eq "ibcmd") {
|
||||||
if (-not $InfoBasePath) {
|
if (-not $InfoBasePath) {
|
||||||
@@ -145,15 +385,21 @@ try {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
$arguments += "--data=$tempDir"
|
$arguments += "--data=$tempDir"
|
||||||
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
$arguments += $extraArgs
|
||||||
$output = & $V8Path @arguments 2>&1
|
Write-Host "Running: ibcmd $((Format-ArgsForDisplay $arguments $engine) -join ' ')"
|
||||||
$exitCode = $LASTEXITCODE
|
$__ib = Invoke-PlatformProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
$ibMissing = ($exitCode -eq 0) -and -not (Test-FileIbCreated $InfoBasePath)
|
||||||
|
if ($ibMissing) { $exitCode = 1 }
|
||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
Write-Host "Information base created successfully: $InfoBasePath" -ForegroundColor Green
|
Write-Host "Information base created successfully: $InfoBasePath" -ForegroundColor Green
|
||||||
|
} elseif ($ibMissing) {
|
||||||
|
Write-Host "Error: exit code 0 but 1Cv8.1CD is missing or empty at $InfoBasePath — information base was not created" -ForegroundColor Red
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error creating information base (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error creating information base (code: $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
if ($output) { Write-Host ($output | Out-String) }
|
Write-PlatformOutput $output
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,6 +407,8 @@ try {
|
|||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
$arguments = @("CREATEINFOBASE")
|
$arguments = @("CREATEINFOBASE")
|
||||||
|
|
||||||
|
# Quotes go INSIDE the token (File="path"): 1C's own parser wants them there, quoting
|
||||||
|
# the whole token instead breaks a path with spaces. Hence -PreQuoted on the launch.
|
||||||
if ($InfoBaseServer -and $InfoBaseRef) {
|
if ($InfoBaseServer -and $InfoBaseRef) {
|
||||||
$arguments += "Srvr=`"$InfoBaseServer`";Ref=`"$InfoBaseRef`""
|
$arguments += "Srvr=`"$InfoBaseServer`";Ref=`"$InfoBaseRef`""
|
||||||
} else {
|
} else {
|
||||||
@@ -185,19 +433,26 @@ try {
|
|||||||
$outFile = Join-Path $tempDir "create_log.txt"
|
$outFile = Join-Path $tempDir "create_log.txt"
|
||||||
$arguments += "/Out", "`"$outFile`""
|
$arguments += "/Out", "`"$outFile`""
|
||||||
$arguments += "/DisableStartupDialogs"
|
$arguments += "/DisableStartupDialogs"
|
||||||
|
$arguments += $extraArgs
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
Write-Host "Running: 1cv8.exe $((Format-ArgsForDisplay $arguments $engine) -join ' ')"
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
|
||||||
$exitCode = $process.ExitCode
|
$exitCode = $__v8.ExitCode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
|
# Postcondition (file infobase only): exit 0 without a non-empty 1Cv8.1CD is a false success.
|
||||||
|
$ibMissing = ($exitCode -eq 0) -and -not ($InfoBaseServer -and $InfoBaseRef) -and -not (Test-FileIbCreated $InfoBasePath)
|
||||||
|
if ($ibMissing) { $exitCode = 1 }
|
||||||
|
|
||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
if ($InfoBaseServer -and $InfoBaseRef) {
|
if ($InfoBaseServer -and $InfoBaseRef) {
|
||||||
Write-Host "Information base created successfully: $InfoBaseServer/$InfoBaseRef" -ForegroundColor Green
|
Write-Host "Information base created successfully: $InfoBaseServer/$InfoBaseRef" -ForegroundColor Green
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Information base created successfully: $InfoBasePath" -ForegroundColor Green
|
Write-Host "Information base created successfully: $InfoBasePath" -ForegroundColor Green
|
||||||
}
|
}
|
||||||
|
} elseif ($ibMissing) {
|
||||||
|
Write-Host "Error: exit code 0 but 1Cv8.1CD is missing or empty at $InfoBasePath — information base was not created" -ForegroundColor Red
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error creating information base (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error creating information base (code: $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
@@ -210,6 +465,7 @@ try {
|
|||||||
Write-Host "--- End ---"
|
Write-Host "--- End ---"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Write-PlatformOutput $__v8.Output
|
||||||
|
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-create v1.4 — Create 1C information base
|
# db-create v1.10 — 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
|
||||||
@@ -36,36 +36,305 @@ def _find_project_v8path():
|
|||||||
d = parent
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
V8_OWNED_KEYS = [
|
||||||
|
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
|
||||||
|
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
|
||||||
|
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
|
||||||
|
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
|
||||||
|
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||||
|
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||||
|
]
|
||||||
|
IBCMD_OWNED_KEYS = [
|
||||||
|
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||||
|
"--import", "--export", "--apply", "--force", "--create-database",
|
||||||
|
"--user", "--password",
|
||||||
|
]
|
||||||
|
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
|
||||||
|
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
|
||||||
|
|
||||||
|
|
||||||
|
def arg_key_match(token, key):
|
||||||
|
"""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
|
||||||
|
/ClearCache distinct from /C."""
|
||||||
|
if len(token) < len(key):
|
||||||
|
return False
|
||||||
|
if token[: len(key)].lower() != key.lower():
|
||||||
|
return False
|
||||||
|
if len(token) == len(key):
|
||||||
|
return True
|
||||||
|
return not token[len(key)].isalpha()
|
||||||
|
|
||||||
|
|
||||||
|
def project_extra_args(name):
|
||||||
|
"""v8args / ibcmdargs from .v8-project.json — same upward walk as v8path."""
|
||||||
|
d = os.getcwd()
|
||||||
|
while True:
|
||||||
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pf):
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
v = data.get(name)
|
||||||
|
if v:
|
||||||
|
return [str(x) for x in v]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return []
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return []
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def assert_extra_args(extra, engine, hints):
|
||||||
|
"""The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
output key fails with an opaque 1C error — reject what the skill owns itself."""
|
||||||
|
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
|
||||||
|
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
|
||||||
|
for tok in extra:
|
||||||
|
if engine == "ibcmd" and not tok.startswith("-"):
|
||||||
|
print(
|
||||||
|
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||||
|
f"({param} cannot extend the ibcmd command)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
for k in owned:
|
||||||
|
if arg_key_match(tok, k):
|
||||||
|
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||||
|
print(
|
||||||
|
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def format_args_for_display(arglist, engine):
|
||||||
|
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
a leaked password does."""
|
||||||
|
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
|
||||||
|
res = []
|
||||||
|
mask_next = False
|
||||||
|
for tok in arglist:
|
||||||
|
if mask_next:
|
||||||
|
res.append("***")
|
||||||
|
mask_next = False
|
||||||
|
continue
|
||||||
|
hit = None
|
||||||
|
for k in keys:
|
||||||
|
if tok[: len(k)].lower() == k.lower():
|
||||||
|
hit = k
|
||||||
|
break
|
||||||
|
if hit is None:
|
||||||
|
res.append(tok)
|
||||||
|
elif len(tok) == len(hit):
|
||||||
|
res.append(tok)
|
||||||
|
mask_next = True
|
||||||
|
elif tok[len(hit)] == "=":
|
||||||
|
res.append(hit + "=***")
|
||||||
|
else:
|
||||||
|
res.append(hit + "***")
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def extract_extra_args(argv, known_opts):
|
||||||
|
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
|
||||||
|
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
|
||||||
|
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
|
||||||
|
rest, v8, ibcmd = [], [], []
|
||||||
|
i = 0
|
||||||
|
while i < len(argv):
|
||||||
|
low = argv[i].lower()
|
||||||
|
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
|
||||||
|
target = v8 if low == "-additionalv8arguments" else ibcmd
|
||||||
|
i += 1
|
||||||
|
while i < len(argv) and argv[i].lower() not in known_opts:
|
||||||
|
target.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
rest.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
return rest, v8, ibcmd
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
||||||
|
"""Pick the argument list for the selected engine and validate it. An explicitly
|
||||||
|
passed parameter for the other engine is an error; the same keys coming from
|
||||||
|
.v8-project.json simply do not apply — a project may describe both engines.
|
||||||
|
|
||||||
|
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
|
||||||
|
so that form is the documented one and both ports must accept it. A value containing
|
||||||
|
a comma is not supported."""
|
||||||
|
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
|
||||||
|
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
|
||||||
|
if engine == "ibcmd" and v8_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||||
|
"(use -AdditionalIbcmdArguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine != "ibcmd" and ibcmd_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||||
|
"(use -AdditionalV8Arguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine == "ibcmd":
|
||||||
|
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
|
||||||
|
else:
|
||||||
|
extra = project_extra_args("v8args") + list(v8_extra)
|
||||||
|
if extra:
|
||||||
|
assert_extra_args(extra, engine, hints)
|
||||||
|
return extra
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
def _version_key(p):
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
"""Numeric sort key from version dir name."""
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def resolve_v8path(v8path):
|
||||||
"""Resolve path to 1cv8.exe."""
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
if not v8path:
|
if not v8path:
|
||||||
v8path = _find_project_v8path()
|
v8path = _find_project_v8path()
|
||||||
if not v8path:
|
if not v8path:
|
||||||
candidates = (
|
if os.name == "nt":
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
candidates = (
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
)
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
if candidates:
|
if candidates:
|
||||||
v8path = max(candidates, key=_version_key)
|
v8path = max(candidates, key=_version_key)
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
else:
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
if os.path.isdir(v8path):
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
def file_ib_created(ib_path):
|
||||||
|
"""File-infobase postcondition: the platform must have produced a non-empty 1Cv8.1CD.
|
||||||
|
Exit code 0 without it (broken/headless env) is a false success — reject it."""
|
||||||
|
f = os.path.join(ib_path, "1Cv8.1CD")
|
||||||
|
return os.path.isfile(f) and os.path.getsize(f) > 0
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def decode_platform_bytes(data):
|
||||||
|
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
|
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — the locale
|
||||||
|
code page (what text=True uses) mangles both."""
|
||||||
|
if not data:
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
return data.decode("utf-8")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
return data.decode("cp866", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def clean_path(value, param=""):
|
||||||
|
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
|
||||||
|
if not value:
|
||||||
|
return value
|
||||||
|
v = value.strip()
|
||||||
|
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
|
||||||
|
v = v[1:-1].strip()
|
||||||
|
if len(v) > 3 and v[-1] in "\\/":
|
||||||
|
v = v[:-1]
|
||||||
|
if '"' in v:
|
||||||
|
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
def quote_if_needed(token):
|
||||||
|
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
|
||||||
|
verbatim, so a token with a space needs quotes of its own."""
|
||||||
|
if token and (" " in token or "\t" in token) and '"' not in token:
|
||||||
|
return f'"{token}"'
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
def run_v8(v8path, arguments):
|
||||||
|
"""Run 1cv8 in batch mode and capture its console output.
|
||||||
|
|
||||||
|
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
|
||||||
|
escape those quotes, so there the command line is handed over ready-made.
|
||||||
|
"""
|
||||||
|
if os.name == "nt":
|
||||||
|
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||||
|
else:
|
||||||
|
cmd = [v8path] + arguments
|
||||||
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def print_platform_output(result):
|
||||||
|
"""Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
|
||||||
|
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
|
||||||
|
if not text:
|
||||||
|
return
|
||||||
|
limit = 65536
|
||||||
|
if len(text) > limit:
|
||||||
|
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
|
||||||
|
print("--- Вывод платформы ---")
|
||||||
|
print(text)
|
||||||
|
print("--- End ---")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
@@ -80,11 +349,33 @@ def main():
|
|||||||
parser.add_argument("-UseTemplate", default="")
|
parser.add_argument("-UseTemplate", default="")
|
||||||
parser.add_argument("-AddToList", action="store_true")
|
parser.add_argument("-AddToList", action="store_true")
|
||||||
parser.add_argument("-ListName", default="")
|
parser.add_argument("-ListName", default="")
|
||||||
args = parser.parse_args()
|
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
|
||||||
|
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
|
||||||
|
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
|
||||||
|
help="Extra ibcmd arguments in --key=value form")
|
||||||
|
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
||||||
|
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
||||||
|
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
||||||
|
args.UseTemplate = clean_path(args.UseTemplate, "-UseTemplate")
|
||||||
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
arg_hints = {
|
||||||
|
"/F": "-InfoBasePath",
|
||||||
|
"/S": "-InfoBaseServer + -InfoBaseRef",
|
||||||
|
"/UseTemplate": "-UseTemplate",
|
||||||
|
"/AddToList": "-AddToList",
|
||||||
|
"--db-path": "-InfoBasePath",
|
||||||
|
"--load": "-UseTemplate",
|
||||||
|
"--restore": "-UseTemplate",
|
||||||
|
}
|
||||||
|
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if engine == "ibcmd":
|
if engine == "ibcmd":
|
||||||
if not args.InfoBasePath:
|
if not args.InfoBasePath:
|
||||||
@@ -110,17 +401,25 @@ def main():
|
|||||||
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
||||||
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
||||||
arguments.append(f"--data={ib_data}")
|
arguments.append(f"--data={ib_data}")
|
||||||
print(f"Running: ibcmd {' '.join(arguments)}")
|
arguments.extend(quote_if_needed(a) for a in extra_args)
|
||||||
result = subprocess.run([v8path] + arguments, capture_output=True, encoding="utf-8", errors="replace")
|
print(f"Running: ibcmd {' '.join(format_args_for_display(arguments, engine))}")
|
||||||
if result.returncode == 0:
|
result = run_ibcmd([v8path] + arguments, warn_no_user=False)
|
||||||
|
exit_code = result.returncode
|
||||||
|
ib_missing = exit_code == 0 and not file_ib_created(args.InfoBasePath)
|
||||||
|
if ib_missing:
|
||||||
|
exit_code = 1
|
||||||
|
if exit_code == 0:
|
||||||
print(f"Information base created successfully: {args.InfoBasePath}")
|
print(f"Information base created successfully: {args.InfoBasePath}")
|
||||||
|
elif ib_missing:
|
||||||
|
print(
|
||||||
|
f"Error: exit code 0 but 1Cv8.1CD is missing or empty at {args.InfoBasePath} "
|
||||||
|
"— information base was not created",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
print(f"Error creating information base (code: {result.returncode})", file=sys.stderr)
|
print(f"Error creating information base (code: {exit_code})", file=sys.stderr)
|
||||||
if result.stdout:
|
print_platform_output(result)
|
||||||
print(result.stdout)
|
sys.exit(exit_code)
|
||||||
if result.stderr:
|
|
||||||
print(result.stderr, file=sys.stderr)
|
|
||||||
sys.exit(result.returncode)
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
# --- Temp dir ---
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_create_{random.randint(0, 999999)}")
|
temp_dir = os.path.join(tempfile.gettempdir(), f"db_create_{random.randint(0, 999999)}")
|
||||||
@@ -130,44 +429,53 @@ def main():
|
|||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
arguments = ["CREATEINFOBASE"]
|
arguments = ["CREATEINFOBASE"]
|
||||||
|
|
||||||
|
# Quotes go INSIDE the token (File="path"): that is where 1C's parser expects them.
|
||||||
|
# Quoting the whole token instead breaks a path with spaces — on both OSes.
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
if args.InfoBaseServer and args.InfoBaseRef:
|
||||||
# No embedded quotes: subprocess quotes the whole token; 1C's argv parser
|
arguments.append(f'Srvr="{args.InfoBaseServer}";Ref="{args.InfoBaseRef}"')
|
||||||
# strips outer quotes. Inner quotes get escaped by list2cmdline and break parsing.
|
|
||||||
arguments.append(f'Srvr={args.InfoBaseServer};Ref={args.InfoBaseRef}')
|
|
||||||
else:
|
else:
|
||||||
arguments.append(f'File={args.InfoBasePath}')
|
arguments.append(f'File="{args.InfoBasePath}"')
|
||||||
|
|
||||||
# --- Template ---
|
# --- Template ---
|
||||||
if args.UseTemplate:
|
if args.UseTemplate:
|
||||||
arguments.extend(["/UseTemplate", args.UseTemplate])
|
arguments.extend(["/UseTemplate", f'"{args.UseTemplate}"'])
|
||||||
|
|
||||||
# --- Add to list ---
|
# --- Add to list ---
|
||||||
if args.AddToList:
|
if args.AddToList:
|
||||||
if args.ListName:
|
if args.ListName:
|
||||||
arguments.extend(["/AddToList", args.ListName])
|
arguments.extend(["/AddToList", f'"{args.ListName}"'])
|
||||||
else:
|
else:
|
||||||
arguments.append("/AddToList")
|
arguments.append("/AddToList")
|
||||||
|
|
||||||
# --- Output ---
|
# --- Output ---
|
||||||
out_file = os.path.join(temp_dir, "create_log.txt")
|
out_file = os.path.join(temp_dir, "create_log.txt")
|
||||||
arguments.extend(["/Out", out_file])
|
arguments.extend(["/Out", f'"{out_file}"'])
|
||||||
arguments.append("/DisableStartupDialogs")
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
arguments.extend(quote_if_needed(a) for a in extra_args)
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
print(f"Running: 1cv8.exe {' '.join(format_args_for_display(arguments, engine))}")
|
||||||
result = subprocess.run(
|
result = run_v8(v8path, arguments)
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
exit_code = result.returncode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
|
# Postcondition (file infobase only): exit 0 without a non-empty 1Cv8.1CD is a false success.
|
||||||
|
is_server = bool(args.InfoBaseServer and args.InfoBaseRef)
|
||||||
|
ib_missing = exit_code == 0 and not is_server and not file_ib_created(args.InfoBasePath)
|
||||||
|
if ib_missing:
|
||||||
|
exit_code = 1
|
||||||
|
|
||||||
if exit_code == 0:
|
if exit_code == 0:
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
if is_server:
|
||||||
print(f"Information base created successfully: {args.InfoBaseServer}/{args.InfoBaseRef}")
|
print(f"Information base created successfully: {args.InfoBaseServer}/{args.InfoBaseRef}")
|
||||||
else:
|
else:
|
||||||
print(f"Information base created successfully: {args.InfoBasePath}")
|
print(f"Information base created successfully: {args.InfoBasePath}")
|
||||||
|
elif ib_missing:
|
||||||
|
print(
|
||||||
|
f"Error: exit code 0 but 1Cv8.1CD is missing or empty at {args.InfoBasePath} "
|
||||||
|
"— 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})", file=sys.stderr)
|
||||||
|
|
||||||
@@ -181,6 +489,7 @@ def main():
|
|||||||
print("--- End ---")
|
print("--- End ---")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
print_platform_output(result)
|
||||||
|
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" <п
|
|||||||
| `-OutputFile <путь>` | да | Путь к выходному CF-файлу |
|
| `-OutputFile <путь>` | да | Путь к выходному CF-файлу |
|
||||||
| `-Extension <имя>` | нет | Выгрузить расширение |
|
| `-Extension <имя>` | нет | Выгрузить расширение |
|
||||||
| `-AllExtensions` | нет | Выгрузить все расширения |
|
| `-AllExtensions` | нет | Выгрузить все расширения |
|
||||||
|
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
|
||||||
|
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
|
||||||
|
|
||||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# db-dump-cf v1.4 — Dump 1C configuration to CF file
|
# db-dump-cf v1.12 — Dump 1C configuration to CF file
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Выгрузка конфигурации 1С в CF-файл
|
Выгрузка конфигурации 1С в CF-файл
|
||||||
@@ -35,6 +36,12 @@
|
|||||||
.PARAMETER AllExtensions
|
.PARAMETER AllExtensions
|
||||||
Выгрузить все расширения
|
Выгрузить все расширения
|
||||||
|
|
||||||
|
.PARAMETER AdditionalV8Arguments
|
||||||
|
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
|
||||||
|
|
||||||
|
.PARAMETER AdditionalIbcmdArguments
|
||||||
|
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
|
||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
.\db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "config.cf"
|
.\db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "config.cf"
|
||||||
|
|
||||||
@@ -69,12 +76,183 @@ param(
|
|||||||
[string]$Extension,
|
[string]$Extension,
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[switch]$AllExtensions
|
[switch]$AllExtensions,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalV8Arguments = @(),
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalIbcmdArguments = @()
|
||||||
)
|
)
|
||||||
|
|
||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
function Protect-Secrets {
|
||||||
|
# Redact literal secret values from a display string (String.Replace is literal, not regex).
|
||||||
|
param([string]$Text, [string[]]$Secrets)
|
||||||
|
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
|
||||||
|
return $Text
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
$script:V8OwnedKeys = @(
|
||||||
|
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
|
||||||
|
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
|
||||||
|
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
|
||||||
|
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
|
||||||
|
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||||
|
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||||
|
)
|
||||||
|
$script:IbcmdOwnedKeys = @(
|
||||||
|
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||||
|
'--import', '--export', '--apply', '--force', '--create-database',
|
||||||
|
'--user', '--password'
|
||||||
|
)
|
||||||
|
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
|
||||||
|
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
|
||||||
|
|
||||||
|
function Test-ArgKeyMatch {
|
||||||
|
# A token matches a key when it equals the key, or starts with it and the next
|
||||||
|
# character is not a letter — catches glued /N"user" and --password=x, while
|
||||||
|
# keeping /ClearCache distinct from /C.
|
||||||
|
param([string]$Token, [string]$Key)
|
||||||
|
if ($Token.Length -lt $Key.Length) { return $false }
|
||||||
|
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
|
||||||
|
if ($Token.Length -eq $Key.Length) { return $true }
|
||||||
|
return -not [char]::IsLetter($Token[$Key.Length])
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ProjectExtraArgs {
|
||||||
|
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
|
||||||
|
param([string]$Name)
|
||||||
|
$dir = (Get-Location).Path
|
||||||
|
while ($dir) {
|
||||||
|
$pf = Join-Path $dir ".v8-project.json"
|
||||||
|
if (Test-Path $pf) {
|
||||||
|
try {
|
||||||
|
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
|
||||||
|
} catch {}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
$parent = Split-Path $dir -Parent
|
||||||
|
if (-not $parent -or $parent -eq $dir) { break }
|
||||||
|
$dir = $parent
|
||||||
|
}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Assert-ExtraArgs {
|
||||||
|
# The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
# output key fails with an opaque 1C error — reject what the skill owns itself.
|
||||||
|
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
|
||||||
|
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
|
||||||
|
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
|
||||||
|
foreach ($tok in $ExtraArgs) {
|
||||||
|
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
|
||||||
|
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
foreach ($k in $owned) {
|
||||||
|
if (Test-ArgKeyMatch $tok $k) {
|
||||||
|
$hint = ''
|
||||||
|
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
|
||||||
|
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-ExtraArgs {
|
||||||
|
# Pick the argument list for the selected engine and validate it. An explicitly passed
|
||||||
|
# parameter for the other engine is an error; the same keys coming from .v8-project.json
|
||||||
|
# simply do not apply — a project may describe both engines.
|
||||||
|
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
|
||||||
|
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
|
||||||
|
# space-separated values spill into positional ones, a comma-joined list arrives as a
|
||||||
|
# single token. So accept the repo's list convention (comma-separated) and split here;
|
||||||
|
# a native array call keeps working. A value containing a comma is not supported.
|
||||||
|
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -eq 'ibcmd') {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
|
||||||
|
} else {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
|
||||||
|
}
|
||||||
|
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
|
||||||
|
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
|
||||||
|
# would nest the array — the tokens would then be glued into one argument.
|
||||||
|
return $extra
|
||||||
|
}
|
||||||
|
|
||||||
|
function Format-ArgsForDisplay {
|
||||||
|
# Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
# a leaked password does.
|
||||||
|
param([string[]]$ArgList, [string]$Engine)
|
||||||
|
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
|
||||||
|
$res = @()
|
||||||
|
$maskNext = $false
|
||||||
|
foreach ($tok in $ArgList) {
|
||||||
|
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
|
||||||
|
$hit = $null
|
||||||
|
foreach ($k in $keys) {
|
||||||
|
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
|
||||||
|
}
|
||||||
|
if (-not $hit) { $res += $tok; continue }
|
||||||
|
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
|
||||||
|
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
|
||||||
|
else { $res += ($hit + '***') }
|
||||||
|
}
|
||||||
|
return ,$res
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-CleanPath {
|
||||||
|
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
# inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
|
||||||
|
param([string]$Value, [string]$ParamName)
|
||||||
|
if (-not $Value) { return $Value }
|
||||||
|
$v = $Value.Trim()
|
||||||
|
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
|
||||||
|
$v = $v.Substring(1, $v.Length - 2).Trim()
|
||||||
|
}
|
||||||
|
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
|
||||||
|
if ($v.Contains('"')) {
|
||||||
|
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
return $v
|
||||||
|
}
|
||||||
|
|
||||||
|
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
|
||||||
|
$InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath'
|
||||||
|
$OutputFile = ConvertTo-CleanPath $OutputFile '-OutputFile'
|
||||||
|
|
||||||
|
function Assert-InfoBaseExists {
|
||||||
|
# These skills work on a ready infobase. Saying so up front beats the platform's
|
||||||
|
# "Неверные или отсутствующие параметры соединения" after a launch.
|
||||||
|
param([string]$Path)
|
||||||
|
if (-not $Path) { return }
|
||||||
|
if (-not (Test-Path (Join-Path $Path "1Cv8.1CD"))) {
|
||||||
|
Write-Host "Error: information base not found at $Path (no 1Cv8.1CD)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert-InfoBaseExists $InfoBasePath
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
function Find-ProjectV8Path {
|
||||||
$dir = (Get-Location).Path
|
$dir = (Get-Location).Path
|
||||||
@@ -105,7 +283,7 @@ if (-not $V8Path) {
|
|||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,13 +292,94 @@ if (Test-Path $V8Path -PathType Container) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
|
function ConvertFrom-PlatformBytes {
|
||||||
|
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
|
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
|
||||||
|
# one of them outright mangles Cyrillic.
|
||||||
|
param([byte[]]$Bytes)
|
||||||
|
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
|
||||||
|
try {
|
||||||
|
$strict = New-Object System.Text.UTF8Encoding($false, $true)
|
||||||
|
return $strict.GetString($Bytes)
|
||||||
|
} catch {
|
||||||
|
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-PlatformProcess {
|
||||||
|
# Run the platform non-interactively and capture its console output. A closed stdin pipe
|
||||||
|
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
|
||||||
|
# text out of our stream until we print it labelled (and out of the wrong encoding).
|
||||||
|
# Returns @{ Output; ExitCode }.
|
||||||
|
#
|
||||||
|
# Quoting differs by engine, so the caller says which it built:
|
||||||
|
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
|
||||||
|
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
|
||||||
|
# which is where 1C's own parser expects them; quoting again breaks the value.
|
||||||
|
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = if ($PreQuoted) {
|
||||||
|
$ProcArgs -join ' '
|
||||||
|
} else {
|
||||||
|
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
}
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
# stderr is drained in parallel: reading the streams one after another deadlocks
|
||||||
|
# as soon as the other one fills its pipe buffer.
|
||||||
|
$errMs = New-Object System.IO.MemoryStream
|
||||||
|
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
|
||||||
|
$outMs = New-Object System.IO.MemoryStream
|
||||||
|
$p.StandardOutput.BaseStream.CopyTo($outMs)
|
||||||
|
$errTask.Wait()
|
||||||
|
$p.WaitForExit()
|
||||||
|
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
|
||||||
|
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-PlatformOutput {
|
||||||
|
# Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
|
||||||
|
param([string]$Text)
|
||||||
|
if (-not $Text) { return }
|
||||||
|
$t = $Text.TrimEnd()
|
||||||
|
if (-not $t) { return }
|
||||||
|
$limit = 65536
|
||||||
|
if ($t.Length -gt $limit) {
|
||||||
|
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
|
||||||
|
}
|
||||||
|
Write-Host "--- Вывод платформы ---"
|
||||||
|
Write-Host $t
|
||||||
|
Write-Host "--- End ---"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function Test-OutputNonEmpty {
|
||||||
|
# Postcondition: the platform must have produced a non-empty output file.
|
||||||
|
# Exit code 0 without it (broken/headless env) is a false success — reject it.
|
||||||
|
param([string]$Path)
|
||||||
|
return (Test-Path $Path -PathType Leaf) -and ((Get-Item $Path -ErrorAction SilentlyContinue).Length -gt 0)
|
||||||
|
}
|
||||||
|
|
||||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' }
|
||||||
|
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if ($engine -eq "ibcmd") {
|
if ($engine -eq "ibcmd") {
|
||||||
if (-not $InfoBasePath) {
|
if (-not $InfoBasePath) {
|
||||||
@@ -155,15 +414,21 @@ try {
|
|||||||
if ($UserName) { $arguments += "--user=$UserName" }
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
if ($Password) { $arguments += "--password=$Password" }
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
$arguments += "--data=$tempDir"
|
$arguments += "--data=$tempDir"
|
||||||
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
$arguments += $extraArgs
|
||||||
$output = & $V8Path @arguments 2>&1
|
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
||||||
$exitCode = $LASTEXITCODE
|
$__ib = Invoke-PlatformProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
$outMissing = ($exitCode -eq 0) -and -not (Test-OutputNonEmpty $OutputFile)
|
||||||
|
if ($outMissing) { $exitCode = 1 }
|
||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
Write-Host "Configuration dumped successfully to: $OutputFile" -ForegroundColor Green
|
Write-Host "Configuration dumped successfully to: $OutputFile" -ForegroundColor Green
|
||||||
|
} elseif ($outMissing) {
|
||||||
|
Write-Host "Error: exit code 0 but no non-empty file at $OutputFile — configuration was not dumped" -ForegroundColor Red
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
if ($output) { Write-Host ($output | Out-String) }
|
Write-PlatformOutput $output
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,15 +458,21 @@ try {
|
|||||||
$outFile = Join-Path $tempDir "dump_cf_log.txt"
|
$outFile = Join-Path $tempDir "dump_cf_log.txt"
|
||||||
$arguments += "/Out", "`"$outFile`""
|
$arguments += "/Out", "`"$outFile`""
|
||||||
$arguments += "/DisableStartupDialogs"
|
$arguments += "/DisableStartupDialogs"
|
||||||
|
$arguments += $extraArgs
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
|
||||||
$exitCode = $process.ExitCode
|
$exitCode = $__v8.ExitCode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
|
# Postcondition: exit 0 without a non-empty output file is a false success.
|
||||||
|
$outMissing = ($exitCode -eq 0) -and -not (Test-OutputNonEmpty $OutputFile)
|
||||||
|
if ($outMissing) { $exitCode = 1 }
|
||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
Write-Host "Configuration dumped successfully to: $OutputFile" -ForegroundColor Green
|
Write-Host "Configuration dumped successfully to: $OutputFile" -ForegroundColor Green
|
||||||
|
} elseif ($outMissing) {
|
||||||
|
Write-Host "Error: exit code 0 but no non-empty file at $OutputFile — configuration was not dumped" -ForegroundColor Red
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
@@ -214,6 +485,7 @@ try {
|
|||||||
Write-Host "--- End ---"
|
Write-Host "--- End ---"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Write-PlatformOutput $__v8.Output
|
||||||
|
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-dump-cf v1.4 — Dump 1C configuration to CF file
|
# db-dump-cf v1.12 — 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
|
||||||
@@ -36,36 +36,323 @@ def _find_project_v8path():
|
|||||||
d = parent
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
V8_OWNED_KEYS = [
|
||||||
|
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
|
||||||
|
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
|
||||||
|
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
|
||||||
|
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
|
||||||
|
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||||
|
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||||
|
]
|
||||||
|
IBCMD_OWNED_KEYS = [
|
||||||
|
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||||
|
"--import", "--export", "--apply", "--force", "--create-database",
|
||||||
|
"--user", "--password",
|
||||||
|
]
|
||||||
|
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
|
||||||
|
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
|
||||||
|
|
||||||
|
|
||||||
|
def arg_key_match(token, key):
|
||||||
|
"""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
|
||||||
|
/ClearCache distinct from /C."""
|
||||||
|
if len(token) < len(key):
|
||||||
|
return False
|
||||||
|
if token[: len(key)].lower() != key.lower():
|
||||||
|
return False
|
||||||
|
if len(token) == len(key):
|
||||||
|
return True
|
||||||
|
return not token[len(key)].isalpha()
|
||||||
|
|
||||||
|
|
||||||
|
def project_extra_args(name):
|
||||||
|
"""v8args / ibcmdargs from .v8-project.json — same upward walk as v8path."""
|
||||||
|
d = os.getcwd()
|
||||||
|
while True:
|
||||||
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pf):
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
v = data.get(name)
|
||||||
|
if v:
|
||||||
|
return [str(x) for x in v]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return []
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return []
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def assert_extra_args(extra, engine, hints):
|
||||||
|
"""The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
output key fails with an opaque 1C error — reject what the skill owns itself."""
|
||||||
|
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
|
||||||
|
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
|
||||||
|
for tok in extra:
|
||||||
|
if engine == "ibcmd" and not tok.startswith("-"):
|
||||||
|
print(
|
||||||
|
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||||
|
f"({param} cannot extend the ibcmd command)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
for k in owned:
|
||||||
|
if arg_key_match(tok, k):
|
||||||
|
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||||
|
print(
|
||||||
|
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def format_args_for_display(arglist, engine):
|
||||||
|
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
a leaked password does."""
|
||||||
|
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
|
||||||
|
res = []
|
||||||
|
mask_next = False
|
||||||
|
for tok in arglist:
|
||||||
|
if mask_next:
|
||||||
|
res.append("***")
|
||||||
|
mask_next = False
|
||||||
|
continue
|
||||||
|
hit = None
|
||||||
|
for k in keys:
|
||||||
|
if tok[: len(k)].lower() == k.lower():
|
||||||
|
hit = k
|
||||||
|
break
|
||||||
|
if hit is None:
|
||||||
|
res.append(tok)
|
||||||
|
elif len(tok) == len(hit):
|
||||||
|
res.append(tok)
|
||||||
|
mask_next = True
|
||||||
|
elif tok[len(hit)] == "=":
|
||||||
|
res.append(hit + "=***")
|
||||||
|
else:
|
||||||
|
res.append(hit + "***")
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def extract_extra_args(argv, known_opts):
|
||||||
|
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
|
||||||
|
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
|
||||||
|
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
|
||||||
|
rest, v8, ibcmd = [], [], []
|
||||||
|
i = 0
|
||||||
|
while i < len(argv):
|
||||||
|
low = argv[i].lower()
|
||||||
|
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
|
||||||
|
target = v8 if low == "-additionalv8arguments" else ibcmd
|
||||||
|
i += 1
|
||||||
|
while i < len(argv) and argv[i].lower() not in known_opts:
|
||||||
|
target.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
rest.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
return rest, v8, ibcmd
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
||||||
|
"""Pick the argument list for the selected engine and validate it. An explicitly
|
||||||
|
passed parameter for the other engine is an error; the same keys coming from
|
||||||
|
.v8-project.json simply do not apply — a project may describe both engines.
|
||||||
|
|
||||||
|
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
|
||||||
|
so that form is the documented one and both ports must accept it. A value containing
|
||||||
|
a comma is not supported."""
|
||||||
|
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
|
||||||
|
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
|
||||||
|
if engine == "ibcmd" and v8_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||||
|
"(use -AdditionalIbcmdArguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine != "ibcmd" and ibcmd_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||||
|
"(use -AdditionalV8Arguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine == "ibcmd":
|
||||||
|
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
|
||||||
|
else:
|
||||||
|
extra = project_extra_args("v8args") + list(v8_extra)
|
||||||
|
if extra:
|
||||||
|
assert_extra_args(extra, engine, hints)
|
||||||
|
return extra
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
def _version_key(p):
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
"""Numeric sort key from version dir name."""
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def resolve_v8path(v8path):
|
||||||
"""Resolve path to 1cv8.exe."""
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
if not v8path:
|
if not v8path:
|
||||||
v8path = _find_project_v8path()
|
v8path = _find_project_v8path()
|
||||||
if not v8path:
|
if not v8path:
|
||||||
candidates = (
|
if os.name == "nt":
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
candidates = (
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
)
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
if candidates:
|
if candidates:
|
||||||
v8path = max(candidates, key=_version_key)
|
v8path = max(candidates, key=_version_key)
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
else:
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
if os.path.isdir(v8path):
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def decode_platform_bytes(data):
|
||||||
|
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
|
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — the locale
|
||||||
|
code page (what text=True uses) mangles both."""
|
||||||
|
if not data:
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
return data.decode("utf-8")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
return data.decode("cp866", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def assert_infobase_exists(path):
|
||||||
|
"""These skills work on a ready infobase. Saying so up front beats the platform's
|
||||||
|
"Неверные или отсутствующие параметры соединения" after a launch."""
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
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)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def clean_path(value, param=""):
|
||||||
|
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
|
||||||
|
if not value:
|
||||||
|
return value
|
||||||
|
v = value.strip()
|
||||||
|
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
|
||||||
|
v = v[1:-1].strip()
|
||||||
|
if len(v) > 3 and v[-1] in "\\/":
|
||||||
|
v = v[:-1]
|
||||||
|
if '"' in v:
|
||||||
|
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
def quote_if_needed(token):
|
||||||
|
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
|
||||||
|
verbatim, so a token with a space needs quotes of its own."""
|
||||||
|
if token and (" " in token or "\t" in token) and '"' not in token:
|
||||||
|
return f'"{token}"'
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
def run_v8(v8path, arguments):
|
||||||
|
"""Run 1cv8 in batch mode and capture its console output.
|
||||||
|
|
||||||
|
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
|
||||||
|
escape those quotes, so there the command line is handed over ready-made.
|
||||||
|
"""
|
||||||
|
if os.name == "nt":
|
||||||
|
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||||
|
else:
|
||||||
|
cmd = [v8path] + arguments
|
||||||
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def print_platform_output(result):
|
||||||
|
"""Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
|
||||||
|
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
|
||||||
|
if not text:
|
||||||
|
return
|
||||||
|
limit = 65536
|
||||||
|
if len(text) > limit:
|
||||||
|
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
|
||||||
|
print("--- Вывод платформы ---")
|
||||||
|
print(text)
|
||||||
|
print("--- End ---")
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def output_nonempty(path):
|
||||||
|
"""Postcondition: the platform must have produced a non-empty output file.
|
||||||
|
Exit code 0 without it (broken/headless env) is a false success — reject it."""
|
||||||
|
return os.path.isfile(path) and os.path.getsize(path) > 0
|
||||||
|
|
||||||
|
|
||||||
|
def _redact(text, *secrets):
|
||||||
|
"""Redact literal secret values (password, user) from a display string —
|
||||||
|
precise, never touches lookalike paths."""
|
||||||
|
for s in secrets:
|
||||||
|
if s:
|
||||||
|
text = text.replace(s, "***")
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
@@ -82,11 +369,34 @@ def main():
|
|||||||
parser.add_argument("-OutputFile", required=True)
|
parser.add_argument("-OutputFile", required=True)
|
||||||
parser.add_argument("-Extension", default="")
|
parser.add_argument("-Extension", default="")
|
||||||
parser.add_argument("-AllExtensions", action="store_true")
|
parser.add_argument("-AllExtensions", action="store_true")
|
||||||
args = parser.parse_args()
|
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
|
||||||
|
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
|
||||||
|
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
|
||||||
|
help="Extra ibcmd arguments in --key=value form")
|
||||||
|
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
||||||
|
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
||||||
|
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
||||||
|
assert_infobase_exists(args.InfoBasePath)
|
||||||
|
args.OutputFile = clean_path(args.OutputFile, "-OutputFile")
|
||||||
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
arg_hints = {
|
||||||
|
"/F": "-InfoBasePath",
|
||||||
|
"/S": "-InfoBaseServer + -InfoBaseRef",
|
||||||
|
"/N": "-UserName",
|
||||||
|
"/P": "-Password",
|
||||||
|
"--db-path": "-InfoBasePath",
|
||||||
|
"--user": "-UserName",
|
||||||
|
"--password": "-Password",
|
||||||
|
}
|
||||||
|
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if engine == "ibcmd":
|
if engine == "ibcmd":
|
||||||
if not args.InfoBasePath:
|
if not args.InfoBasePath:
|
||||||
@@ -117,17 +427,20 @@ def main():
|
|||||||
if args.Password:
|
if args.Password:
|
||||||
arguments.append(f"--password={args.Password}")
|
arguments.append(f"--password={args.Password}")
|
||||||
arguments.append(f"--data={ib_data}")
|
arguments.append(f"--data={ib_data}")
|
||||||
print(f"Running: ibcmd {' '.join(arguments)}")
|
arguments.extend(extra_args)
|
||||||
result = subprocess.run([v8path] + arguments, capture_output=True, encoding="utf-8", errors="replace")
|
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||||
if result.returncode == 0:
|
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||||
|
exit_code = result.returncode
|
||||||
|
out_missing = exit_code == 0 and not output_nonempty(args.OutputFile)
|
||||||
|
if out_missing:
|
||||||
|
exit_code = 1
|
||||||
|
if exit_code == 0:
|
||||||
print(f"Configuration dumped successfully to: {args.OutputFile}")
|
print(f"Configuration dumped successfully to: {args.OutputFile}")
|
||||||
|
elif out_missing:
|
||||||
|
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — configuration was not dumped", file=sys.stderr)
|
||||||
else:
|
else:
|
||||||
print(f"Error dumping configuration (code: {result.returncode})", file=sys.stderr)
|
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
|
||||||
if result.stdout:
|
sys.exit(exit_code)
|
||||||
print(result.stdout)
|
|
||||||
if result.stderr:
|
|
||||||
print(result.stderr, file=sys.stderr)
|
|
||||||
sys.exit(result.returncode)
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
# --- Temp dir ---
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_cf_{random.randint(0, 999999)}")
|
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_cf_{random.randint(0, 999999)}")
|
||||||
@@ -138,40 +451,43 @@ def main():
|
|||||||
arguments = ["DESIGNER"]
|
arguments = ["DESIGNER"]
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
if args.InfoBaseServer and args.InfoBaseRef:
|
||||||
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
|
arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
|
||||||
else:
|
else:
|
||||||
arguments.extend(["/F", args.InfoBasePath])
|
arguments.extend(["/F", f'"{args.InfoBasePath}"'])
|
||||||
|
|
||||||
if args.UserName:
|
if args.UserName:
|
||||||
arguments.append(f"/N{args.UserName}")
|
arguments.append(f'/N"{args.UserName}"')
|
||||||
if args.Password:
|
if args.Password:
|
||||||
arguments.append(f"/P{args.Password}")
|
arguments.append(f'/P"{args.Password}"')
|
||||||
|
|
||||||
arguments.extend(["/DumpCfg", args.OutputFile])
|
arguments.extend(["/DumpCfg", f'"{args.OutputFile}"'])
|
||||||
|
|
||||||
# --- Extensions ---
|
# --- Extensions ---
|
||||||
if args.Extension:
|
if args.Extension:
|
||||||
arguments.extend(["-Extension", args.Extension])
|
arguments.extend(["-Extension", f'"{args.Extension}"'])
|
||||||
elif args.AllExtensions:
|
elif args.AllExtensions:
|
||||||
arguments.append("-AllExtensions")
|
arguments.append("-AllExtensions")
|
||||||
|
|
||||||
# --- Output ---
|
# --- Output ---
|
||||||
out_file = os.path.join(temp_dir, "dump_cf_log.txt")
|
out_file = os.path.join(temp_dir, "dump_cf_log.txt")
|
||||||
arguments.extend(["/Out", out_file])
|
arguments.extend(["/Out", f'"{out_file}"'])
|
||||||
arguments.append("/DisableStartupDialogs")
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
arguments.extend(quote_if_needed(a) for a in extra_args)
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||||
result = subprocess.run(
|
result = run_v8(v8path, arguments)
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
exit_code = result.returncode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
|
# Postcondition: exit 0 without a non-empty output file is a false success.
|
||||||
|
out_missing = exit_code == 0 and not output_nonempty(args.OutputFile)
|
||||||
|
if out_missing:
|
||||||
|
exit_code = 1
|
||||||
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:
|
||||||
|
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — configuration was not dumped", file=sys.stderr)
|
||||||
else:
|
else:
|
||||||
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
|
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
|
||||||
|
|
||||||
@@ -186,6 +502,7 @@ def main():
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
print_platform_output(result)
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" <п
|
|||||||
| `-UserName <имя>` | нет | Имя пользователя |
|
| `-UserName <имя>` | нет | Имя пользователя |
|
||||||
| `-Password <пароль>` | нет | Пароль |
|
| `-Password <пароль>` | нет | Пароль |
|
||||||
| `-OutputFile <путь>` | да | Путь к выходному DT-файлу |
|
| `-OutputFile <путь>` | да | Путь к выходному DT-файлу |
|
||||||
|
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
|
||||||
|
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
|
||||||
|
|
||||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# db-dump-dt v1.3 — Dump 1C information base to DT file
|
# db-dump-dt v1.11 — 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 не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Выгрузка информационной базы 1С в DT-файл
|
Выгрузка информационной базы 1С в DT-файл
|
||||||
@@ -28,6 +29,12 @@
|
|||||||
.PARAMETER OutputFile
|
.PARAMETER OutputFile
|
||||||
Путь к выходному DT-файлу
|
Путь к выходному DT-файлу
|
||||||
|
|
||||||
|
.PARAMETER AdditionalV8Arguments
|
||||||
|
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
|
||||||
|
|
||||||
|
.PARAMETER AdditionalIbcmdArguments
|
||||||
|
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
|
||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
.\db-dump-dt.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "backup.dt"
|
.\db-dump-dt.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "backup.dt"
|
||||||
#>
|
#>
|
||||||
@@ -53,12 +60,183 @@ param(
|
|||||||
[string]$Password,
|
[string]$Password,
|
||||||
|
|
||||||
[Parameter(Mandatory=$true)]
|
[Parameter(Mandatory=$true)]
|
||||||
[string]$OutputFile
|
[string]$OutputFile,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalV8Arguments = @(),
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalIbcmdArguments = @()
|
||||||
)
|
)
|
||||||
|
|
||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
function Protect-Secrets {
|
||||||
|
# Redact literal secret values from a display string (String.Replace is literal, not regex).
|
||||||
|
param([string]$Text, [string[]]$Secrets)
|
||||||
|
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
|
||||||
|
return $Text
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
$script:V8OwnedKeys = @(
|
||||||
|
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
|
||||||
|
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
|
||||||
|
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
|
||||||
|
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
|
||||||
|
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||||
|
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||||
|
)
|
||||||
|
$script:IbcmdOwnedKeys = @(
|
||||||
|
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||||
|
'--import', '--export', '--apply', '--force', '--create-database',
|
||||||
|
'--user', '--password'
|
||||||
|
)
|
||||||
|
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
|
||||||
|
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
|
||||||
|
|
||||||
|
function Test-ArgKeyMatch {
|
||||||
|
# A token matches a key when it equals the key, or starts with it and the next
|
||||||
|
# character is not a letter — catches glued /N"user" and --password=x, while
|
||||||
|
# keeping /ClearCache distinct from /C.
|
||||||
|
param([string]$Token, [string]$Key)
|
||||||
|
if ($Token.Length -lt $Key.Length) { return $false }
|
||||||
|
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
|
||||||
|
if ($Token.Length -eq $Key.Length) { return $true }
|
||||||
|
return -not [char]::IsLetter($Token[$Key.Length])
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ProjectExtraArgs {
|
||||||
|
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
|
||||||
|
param([string]$Name)
|
||||||
|
$dir = (Get-Location).Path
|
||||||
|
while ($dir) {
|
||||||
|
$pf = Join-Path $dir ".v8-project.json"
|
||||||
|
if (Test-Path $pf) {
|
||||||
|
try {
|
||||||
|
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
|
||||||
|
} catch {}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
$parent = Split-Path $dir -Parent
|
||||||
|
if (-not $parent -or $parent -eq $dir) { break }
|
||||||
|
$dir = $parent
|
||||||
|
}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Assert-ExtraArgs {
|
||||||
|
# The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
# output key fails with an opaque 1C error — reject what the skill owns itself.
|
||||||
|
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
|
||||||
|
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
|
||||||
|
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
|
||||||
|
foreach ($tok in $ExtraArgs) {
|
||||||
|
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
|
||||||
|
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
foreach ($k in $owned) {
|
||||||
|
if (Test-ArgKeyMatch $tok $k) {
|
||||||
|
$hint = ''
|
||||||
|
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
|
||||||
|
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-ExtraArgs {
|
||||||
|
# Pick the argument list for the selected engine and validate it. An explicitly passed
|
||||||
|
# parameter for the other engine is an error; the same keys coming from .v8-project.json
|
||||||
|
# simply do not apply — a project may describe both engines.
|
||||||
|
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
|
||||||
|
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
|
||||||
|
# space-separated values spill into positional ones, a comma-joined list arrives as a
|
||||||
|
# single token. So accept the repo's list convention (comma-separated) and split here;
|
||||||
|
# a native array call keeps working. A value containing a comma is not supported.
|
||||||
|
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -eq 'ibcmd') {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
|
||||||
|
} else {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
|
||||||
|
}
|
||||||
|
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
|
||||||
|
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
|
||||||
|
# would nest the array — the tokens would then be glued into one argument.
|
||||||
|
return $extra
|
||||||
|
}
|
||||||
|
|
||||||
|
function Format-ArgsForDisplay {
|
||||||
|
# Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
# a leaked password does.
|
||||||
|
param([string[]]$ArgList, [string]$Engine)
|
||||||
|
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
|
||||||
|
$res = @()
|
||||||
|
$maskNext = $false
|
||||||
|
foreach ($tok in $ArgList) {
|
||||||
|
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
|
||||||
|
$hit = $null
|
||||||
|
foreach ($k in $keys) {
|
||||||
|
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
|
||||||
|
}
|
||||||
|
if (-not $hit) { $res += $tok; continue }
|
||||||
|
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
|
||||||
|
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
|
||||||
|
else { $res += ($hit + '***') }
|
||||||
|
}
|
||||||
|
return ,$res
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-CleanPath {
|
||||||
|
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
# inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
|
||||||
|
param([string]$Value, [string]$ParamName)
|
||||||
|
if (-not $Value) { return $Value }
|
||||||
|
$v = $Value.Trim()
|
||||||
|
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
|
||||||
|
$v = $v.Substring(1, $v.Length - 2).Trim()
|
||||||
|
}
|
||||||
|
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
|
||||||
|
if ($v.Contains('"')) {
|
||||||
|
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
return $v
|
||||||
|
}
|
||||||
|
|
||||||
|
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
|
||||||
|
$InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath'
|
||||||
|
$OutputFile = ConvertTo-CleanPath $OutputFile '-OutputFile'
|
||||||
|
|
||||||
|
function Assert-InfoBaseExists {
|
||||||
|
# These skills work on a ready infobase. Saying so up front beats the platform's
|
||||||
|
# "Неверные или отсутствующие параметры соединения" after a launch.
|
||||||
|
param([string]$Path)
|
||||||
|
if (-not $Path) { return }
|
||||||
|
if (-not (Test-Path (Join-Path $Path "1Cv8.1CD"))) {
|
||||||
|
Write-Host "Error: information base not found at $Path (no 1Cv8.1CD)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert-InfoBaseExists $InfoBasePath
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
function Find-ProjectV8Path {
|
||||||
$dir = (Get-Location).Path
|
$dir = (Get-Location).Path
|
||||||
@@ -89,7 +267,7 @@ if (-not $V8Path) {
|
|||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,13 +276,94 @@ if (Test-Path $V8Path -PathType Container) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
|
function ConvertFrom-PlatformBytes {
|
||||||
|
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
|
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
|
||||||
|
# one of them outright mangles Cyrillic.
|
||||||
|
param([byte[]]$Bytes)
|
||||||
|
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
|
||||||
|
try {
|
||||||
|
$strict = New-Object System.Text.UTF8Encoding($false, $true)
|
||||||
|
return $strict.GetString($Bytes)
|
||||||
|
} catch {
|
||||||
|
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-PlatformProcess {
|
||||||
|
# Run the platform non-interactively and capture its console output. A closed stdin pipe
|
||||||
|
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
|
||||||
|
# text out of our stream until we print it labelled (and out of the wrong encoding).
|
||||||
|
# Returns @{ Output; ExitCode }.
|
||||||
|
#
|
||||||
|
# Quoting differs by engine, so the caller says which it built:
|
||||||
|
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
|
||||||
|
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
|
||||||
|
# which is where 1C's own parser expects them; quoting again breaks the value.
|
||||||
|
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = if ($PreQuoted) {
|
||||||
|
$ProcArgs -join ' '
|
||||||
|
} else {
|
||||||
|
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
}
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
# stderr is drained in parallel: reading the streams one after another deadlocks
|
||||||
|
# as soon as the other one fills its pipe buffer.
|
||||||
|
$errMs = New-Object System.IO.MemoryStream
|
||||||
|
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
|
||||||
|
$outMs = New-Object System.IO.MemoryStream
|
||||||
|
$p.StandardOutput.BaseStream.CopyTo($outMs)
|
||||||
|
$errTask.Wait()
|
||||||
|
$p.WaitForExit()
|
||||||
|
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
|
||||||
|
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-PlatformOutput {
|
||||||
|
# Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
|
||||||
|
param([string]$Text)
|
||||||
|
if (-not $Text) { return }
|
||||||
|
$t = $Text.TrimEnd()
|
||||||
|
if (-not $t) { return }
|
||||||
|
$limit = 65536
|
||||||
|
if ($t.Length -gt $limit) {
|
||||||
|
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
|
||||||
|
}
|
||||||
|
Write-Host "--- Вывод платформы ---"
|
||||||
|
Write-Host $t
|
||||||
|
Write-Host "--- End ---"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function Test-OutputNonEmpty {
|
||||||
|
# Postcondition: the platform must have produced a non-empty output file.
|
||||||
|
# Exit code 0 without it (broken/headless env) is a false success — reject it.
|
||||||
|
param([string]$Path)
|
||||||
|
return (Test-Path $Path -PathType Leaf) -and ((Get-Item $Path -ErrorAction SilentlyContinue).Length -gt 0)
|
||||||
|
}
|
||||||
|
|
||||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' }
|
||||||
|
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if ($engine -eq "ibcmd") {
|
if ($engine -eq "ibcmd") {
|
||||||
if (-not $InfoBasePath) {
|
if (-not $InfoBasePath) {
|
||||||
@@ -135,15 +394,22 @@ try {
|
|||||||
$arguments += "$OutputFile"
|
$arguments += "$OutputFile"
|
||||||
|
|
||||||
$arguments += "--data=$tempDir"
|
$arguments += "--data=$tempDir"
|
||||||
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
|
||||||
$output = & $V8Path @arguments 2>&1
|
$arguments += $extraArgs
|
||||||
$exitCode = $LASTEXITCODE
|
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
||||||
|
$__ib = Invoke-PlatformProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
$outMissing = ($exitCode -eq 0) -and -not (Test-OutputNonEmpty $OutputFile)
|
||||||
|
if ($outMissing) { $exitCode = 1 }
|
||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
Write-Host "Information base dumped successfully to: $OutputFile" -ForegroundColor Green
|
Write-Host "Information base dumped successfully to: $OutputFile" -ForegroundColor Green
|
||||||
|
} elseif ($outMissing) {
|
||||||
|
Write-Host "Error: exit code 0 but no non-empty file at $OutputFile — information base was not dumped" -ForegroundColor Red
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error dumping information base (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error dumping information base (code: $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
if ($output) { Write-Host ($output | Out-String) }
|
Write-PlatformOutput $output
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,15 +432,21 @@ try {
|
|||||||
$outFile = Join-Path $tempDir "dump_dt_log.txt"
|
$outFile = Join-Path $tempDir "dump_dt_log.txt"
|
||||||
$arguments += "/Out", "`"$outFile`""
|
$arguments += "/Out", "`"$outFile`""
|
||||||
$arguments += "/DisableStartupDialogs"
|
$arguments += "/DisableStartupDialogs"
|
||||||
|
$arguments += $extraArgs
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
|
||||||
$exitCode = $process.ExitCode
|
$exitCode = $__v8.ExitCode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
|
# Postcondition: exit 0 without a non-empty output file is a false success.
|
||||||
|
$outMissing = ($exitCode -eq 0) -and -not (Test-OutputNonEmpty $OutputFile)
|
||||||
|
if ($outMissing) { $exitCode = 1 }
|
||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
Write-Host "Information base dumped successfully to: $OutputFile" -ForegroundColor Green
|
Write-Host "Information base dumped successfully to: $OutputFile" -ForegroundColor Green
|
||||||
|
} elseif ($outMissing) {
|
||||||
|
Write-Host "Error: exit code 0 but no non-empty file at $OutputFile — information base was not dumped" -ForegroundColor Red
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error dumping information base (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error dumping information base (code: $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
@@ -187,6 +459,7 @@ try {
|
|||||||
Write-Host "--- End ---"
|
Write-Host "--- End ---"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Write-PlatformOutput $__v8.Output
|
||||||
|
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-dump-dt v1.3 — Dump 1C information base to DT file
|
# db-dump-dt v1.11 — 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
|
||||||
@@ -36,36 +36,323 @@ def _find_project_v8path():
|
|||||||
d = parent
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
V8_OWNED_KEYS = [
|
||||||
|
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
|
||||||
|
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
|
||||||
|
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
|
||||||
|
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
|
||||||
|
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||||
|
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||||
|
]
|
||||||
|
IBCMD_OWNED_KEYS = [
|
||||||
|
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||||
|
"--import", "--export", "--apply", "--force", "--create-database",
|
||||||
|
"--user", "--password",
|
||||||
|
]
|
||||||
|
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
|
||||||
|
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
|
||||||
|
|
||||||
|
|
||||||
|
def arg_key_match(token, key):
|
||||||
|
"""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
|
||||||
|
/ClearCache distinct from /C."""
|
||||||
|
if len(token) < len(key):
|
||||||
|
return False
|
||||||
|
if token[: len(key)].lower() != key.lower():
|
||||||
|
return False
|
||||||
|
if len(token) == len(key):
|
||||||
|
return True
|
||||||
|
return not token[len(key)].isalpha()
|
||||||
|
|
||||||
|
|
||||||
|
def project_extra_args(name):
|
||||||
|
"""v8args / ibcmdargs from .v8-project.json — same upward walk as v8path."""
|
||||||
|
d = os.getcwd()
|
||||||
|
while True:
|
||||||
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pf):
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
v = data.get(name)
|
||||||
|
if v:
|
||||||
|
return [str(x) for x in v]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return []
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return []
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def assert_extra_args(extra, engine, hints):
|
||||||
|
"""The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
output key fails with an opaque 1C error — reject what the skill owns itself."""
|
||||||
|
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
|
||||||
|
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
|
||||||
|
for tok in extra:
|
||||||
|
if engine == "ibcmd" and not tok.startswith("-"):
|
||||||
|
print(
|
||||||
|
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||||
|
f"({param} cannot extend the ibcmd command)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
for k in owned:
|
||||||
|
if arg_key_match(tok, k):
|
||||||
|
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||||
|
print(
|
||||||
|
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def format_args_for_display(arglist, engine):
|
||||||
|
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
a leaked password does."""
|
||||||
|
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
|
||||||
|
res = []
|
||||||
|
mask_next = False
|
||||||
|
for tok in arglist:
|
||||||
|
if mask_next:
|
||||||
|
res.append("***")
|
||||||
|
mask_next = False
|
||||||
|
continue
|
||||||
|
hit = None
|
||||||
|
for k in keys:
|
||||||
|
if tok[: len(k)].lower() == k.lower():
|
||||||
|
hit = k
|
||||||
|
break
|
||||||
|
if hit is None:
|
||||||
|
res.append(tok)
|
||||||
|
elif len(tok) == len(hit):
|
||||||
|
res.append(tok)
|
||||||
|
mask_next = True
|
||||||
|
elif tok[len(hit)] == "=":
|
||||||
|
res.append(hit + "=***")
|
||||||
|
else:
|
||||||
|
res.append(hit + "***")
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def extract_extra_args(argv, known_opts):
|
||||||
|
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
|
||||||
|
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
|
||||||
|
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
|
||||||
|
rest, v8, ibcmd = [], [], []
|
||||||
|
i = 0
|
||||||
|
while i < len(argv):
|
||||||
|
low = argv[i].lower()
|
||||||
|
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
|
||||||
|
target = v8 if low == "-additionalv8arguments" else ibcmd
|
||||||
|
i += 1
|
||||||
|
while i < len(argv) and argv[i].lower() not in known_opts:
|
||||||
|
target.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
rest.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
return rest, v8, ibcmd
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
||||||
|
"""Pick the argument list for the selected engine and validate it. An explicitly
|
||||||
|
passed parameter for the other engine is an error; the same keys coming from
|
||||||
|
.v8-project.json simply do not apply — a project may describe both engines.
|
||||||
|
|
||||||
|
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
|
||||||
|
so that form is the documented one and both ports must accept it. A value containing
|
||||||
|
a comma is not supported."""
|
||||||
|
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
|
||||||
|
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
|
||||||
|
if engine == "ibcmd" and v8_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||||
|
"(use -AdditionalIbcmdArguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine != "ibcmd" and ibcmd_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||||
|
"(use -AdditionalV8Arguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine == "ibcmd":
|
||||||
|
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
|
||||||
|
else:
|
||||||
|
extra = project_extra_args("v8args") + list(v8_extra)
|
||||||
|
if extra:
|
||||||
|
assert_extra_args(extra, engine, hints)
|
||||||
|
return extra
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
def _version_key(p):
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
"""Numeric sort key from version dir name."""
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def resolve_v8path(v8path):
|
||||||
"""Resolve path to 1cv8.exe."""
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
if not v8path:
|
if not v8path:
|
||||||
v8path = _find_project_v8path()
|
v8path = _find_project_v8path()
|
||||||
if not v8path:
|
if not v8path:
|
||||||
candidates = (
|
if os.name == "nt":
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
candidates = (
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
)
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
if candidates:
|
if candidates:
|
||||||
v8path = max(candidates, key=_version_key)
|
v8path = max(candidates, key=_version_key)
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
else:
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
if os.path.isdir(v8path):
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def decode_platform_bytes(data):
|
||||||
|
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
|
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — the locale
|
||||||
|
code page (what text=True uses) mangles both."""
|
||||||
|
if not data:
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
return data.decode("utf-8")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
return data.decode("cp866", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def assert_infobase_exists(path):
|
||||||
|
"""These skills work on a ready infobase. Saying so up front beats the platform's
|
||||||
|
"Неверные или отсутствующие параметры соединения" after a launch."""
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
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)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def clean_path(value, param=""):
|
||||||
|
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
|
||||||
|
if not value:
|
||||||
|
return value
|
||||||
|
v = value.strip()
|
||||||
|
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
|
||||||
|
v = v[1:-1].strip()
|
||||||
|
if len(v) > 3 and v[-1] in "\\/":
|
||||||
|
v = v[:-1]
|
||||||
|
if '"' in v:
|
||||||
|
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
def quote_if_needed(token):
|
||||||
|
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
|
||||||
|
verbatim, so a token with a space needs quotes of its own."""
|
||||||
|
if token and (" " in token or "\t" in token) and '"' not in token:
|
||||||
|
return f'"{token}"'
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
def run_v8(v8path, arguments):
|
||||||
|
"""Run 1cv8 in batch mode and capture its console output.
|
||||||
|
|
||||||
|
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
|
||||||
|
escape those quotes, so there the command line is handed over ready-made.
|
||||||
|
"""
|
||||||
|
if os.name == "nt":
|
||||||
|
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||||
|
else:
|
||||||
|
cmd = [v8path] + arguments
|
||||||
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def print_platform_output(result):
|
||||||
|
"""Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
|
||||||
|
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
|
||||||
|
if not text:
|
||||||
|
return
|
||||||
|
limit = 65536
|
||||||
|
if len(text) > limit:
|
||||||
|
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
|
||||||
|
print("--- Вывод платформы ---")
|
||||||
|
print(text)
|
||||||
|
print("--- End ---")
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def output_nonempty(path):
|
||||||
|
"""Postcondition: the platform must have produced a non-empty output file.
|
||||||
|
Exit code 0 without it (broken/headless env) is a false success — reject it."""
|
||||||
|
return os.path.isfile(path) and os.path.getsize(path) > 0
|
||||||
|
|
||||||
|
|
||||||
|
def _redact(text, *secrets):
|
||||||
|
"""Redact literal secret values (password, user) from a display string —
|
||||||
|
precise, never touches lookalike paths."""
|
||||||
|
for s in secrets:
|
||||||
|
if s:
|
||||||
|
text = text.replace(s, "***")
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
@@ -80,11 +367,34 @@ def main():
|
|||||||
parser.add_argument("-UserName", default="")
|
parser.add_argument("-UserName", default="")
|
||||||
parser.add_argument("-Password", default="")
|
parser.add_argument("-Password", default="")
|
||||||
parser.add_argument("-OutputFile", required=True)
|
parser.add_argument("-OutputFile", required=True)
|
||||||
args = parser.parse_args()
|
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
|
||||||
|
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
|
||||||
|
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
|
||||||
|
help="Extra ibcmd arguments in --key=value form")
|
||||||
|
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
||||||
|
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
||||||
|
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
||||||
|
assert_infobase_exists(args.InfoBasePath)
|
||||||
|
args.OutputFile = clean_path(args.OutputFile, "-OutputFile")
|
||||||
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
arg_hints = {
|
||||||
|
"/F": "-InfoBasePath",
|
||||||
|
"/S": "-InfoBaseServer + -InfoBaseRef",
|
||||||
|
"/N": "-UserName",
|
||||||
|
"/P": "-Password",
|
||||||
|
"--db-path": "-InfoBasePath",
|
||||||
|
"--user": "-UserName",
|
||||||
|
"--password": "-Password",
|
||||||
|
}
|
||||||
|
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if engine == "ibcmd":
|
if engine == "ibcmd":
|
||||||
if not args.InfoBasePath:
|
if not args.InfoBasePath:
|
||||||
@@ -110,17 +420,20 @@ def main():
|
|||||||
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
||||||
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
||||||
arguments.append(f"--data={ib_data}")
|
arguments.append(f"--data={ib_data}")
|
||||||
print(f"Running: ibcmd {' '.join(arguments)}")
|
arguments.extend(extra_args)
|
||||||
result = subprocess.run([v8path] + arguments, capture_output=True, encoding="utf-8", errors="replace")
|
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||||
if result.returncode == 0:
|
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||||
|
exit_code = result.returncode
|
||||||
|
out_missing = exit_code == 0 and not output_nonempty(args.OutputFile)
|
||||||
|
if out_missing:
|
||||||
|
exit_code = 1
|
||||||
|
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:
|
||||||
|
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — information base was not dumped", file=sys.stderr)
|
||||||
else:
|
else:
|
||||||
print(f"Error dumping information base (code: {result.returncode})", file=sys.stderr)
|
print(f"Error dumping information base (code: {exit_code})", file=sys.stderr)
|
||||||
if result.stdout:
|
sys.exit(exit_code)
|
||||||
print(result.stdout)
|
|
||||||
if result.stderr:
|
|
||||||
print(result.stderr, file=sys.stderr)
|
|
||||||
sys.exit(result.returncode)
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
# --- Temp dir ---
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_dt_{random.randint(0, 999999)}")
|
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_dt_{random.randint(0, 999999)}")
|
||||||
@@ -131,34 +444,37 @@ def main():
|
|||||||
arguments = ["DESIGNER"]
|
arguments = ["DESIGNER"]
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
if args.InfoBaseServer and args.InfoBaseRef:
|
||||||
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
|
arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
|
||||||
else:
|
else:
|
||||||
arguments.extend(["/F", args.InfoBasePath])
|
arguments.extend(["/F", f'"{args.InfoBasePath}"'])
|
||||||
|
|
||||||
if args.UserName:
|
if args.UserName:
|
||||||
arguments.append(f"/N{args.UserName}")
|
arguments.append(f'/N"{args.UserName}"')
|
||||||
if args.Password:
|
if args.Password:
|
||||||
arguments.append(f"/P{args.Password}")
|
arguments.append(f'/P"{args.Password}"')
|
||||||
|
|
||||||
arguments.extend(["/DumpIB", args.OutputFile])
|
arguments.extend(["/DumpIB", f'"{args.OutputFile}"'])
|
||||||
|
|
||||||
# --- Output ---
|
# --- Output ---
|
||||||
out_file = os.path.join(temp_dir, "dump_dt_log.txt")
|
out_file = os.path.join(temp_dir, "dump_dt_log.txt")
|
||||||
arguments.extend(["/Out", out_file])
|
arguments.extend(["/Out", f'"{out_file}"'])
|
||||||
arguments.append("/DisableStartupDialogs")
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
arguments.extend(quote_if_needed(a) for a in extra_args)
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||||
result = subprocess.run(
|
result = run_v8(v8path, arguments)
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
exit_code = result.returncode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
|
# Postcondition: exit 0 without a non-empty output file is a false success.
|
||||||
|
out_missing = exit_code == 0 and not output_nonempty(args.OutputFile)
|
||||||
|
if out_missing:
|
||||||
|
exit_code = 1
|
||||||
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:
|
||||||
|
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — information base was not dumped", file=sys.stderr)
|
||||||
else:
|
else:
|
||||||
print(f"Error dumping information base (code: {exit_code})", file=sys.stderr)
|
print(f"Error dumping information base (code: {exit_code})", file=sys.stderr)
|
||||||
|
|
||||||
@@ -173,6 +489,7 @@ def main():
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
print_platform_output(result)
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" <
|
|||||||
| `-Extension <имя>` | нет | Выгрузить расширение |
|
| `-Extension <имя>` | нет | Выгрузить расширение |
|
||||||
| `-AllExtensions` | нет | Выгрузить все расширения |
|
| `-AllExtensions` | нет | Выгрузить все расширения |
|
||||||
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
|
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
|
||||||
|
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
|
||||||
|
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
|
||||||
|
|
||||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# db-dump-xml v1.6 — Dump 1C configuration to XML files
|
# db-dump-xml v1.14 — Dump 1C configuration to XML files
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Выгрузка конфигурации 1С в XML-файлы
|
Выгрузка конфигурации 1С в XML-файлы
|
||||||
@@ -47,6 +48,12 @@
|
|||||||
.PARAMETER Format
|
.PARAMETER Format
|
||||||
Формат выгрузки: Hierarchical или Plain (по умолчанию Hierarchical)
|
Формат выгрузки: Hierarchical или Plain (по умолчанию Hierarchical)
|
||||||
|
|
||||||
|
.PARAMETER AdditionalV8Arguments
|
||||||
|
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
|
||||||
|
|
||||||
|
.PARAMETER AdditionalIbcmdArguments
|
||||||
|
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
|
||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
.\db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Full
|
.\db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Full
|
||||||
|
|
||||||
@@ -92,12 +99,183 @@ param(
|
|||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[ValidateSet("Hierarchical", "Plain")]
|
[ValidateSet("Hierarchical", "Plain")]
|
||||||
[string]$Format = "Hierarchical"
|
[string]$Format = "Hierarchical",
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalV8Arguments = @(),
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalIbcmdArguments = @()
|
||||||
)
|
)
|
||||||
|
|
||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
function Protect-Secrets {
|
||||||
|
# Redact literal secret values from a display string (String.Replace is literal, not regex).
|
||||||
|
param([string]$Text, [string[]]$Secrets)
|
||||||
|
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
|
||||||
|
return $Text
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
$script:V8OwnedKeys = @(
|
||||||
|
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
|
||||||
|
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
|
||||||
|
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
|
||||||
|
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
|
||||||
|
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||||
|
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||||
|
)
|
||||||
|
$script:IbcmdOwnedKeys = @(
|
||||||
|
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||||
|
'--import', '--export', '--apply', '--force', '--create-database',
|
||||||
|
'--user', '--password'
|
||||||
|
)
|
||||||
|
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
|
||||||
|
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
|
||||||
|
|
||||||
|
function Test-ArgKeyMatch {
|
||||||
|
# A token matches a key when it equals the key, or starts with it and the next
|
||||||
|
# character is not a letter — catches glued /N"user" and --password=x, while
|
||||||
|
# keeping /ClearCache distinct from /C.
|
||||||
|
param([string]$Token, [string]$Key)
|
||||||
|
if ($Token.Length -lt $Key.Length) { return $false }
|
||||||
|
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
|
||||||
|
if ($Token.Length -eq $Key.Length) { return $true }
|
||||||
|
return -not [char]::IsLetter($Token[$Key.Length])
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ProjectExtraArgs {
|
||||||
|
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
|
||||||
|
param([string]$Name)
|
||||||
|
$dir = (Get-Location).Path
|
||||||
|
while ($dir) {
|
||||||
|
$pf = Join-Path $dir ".v8-project.json"
|
||||||
|
if (Test-Path $pf) {
|
||||||
|
try {
|
||||||
|
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
|
||||||
|
} catch {}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
$parent = Split-Path $dir -Parent
|
||||||
|
if (-not $parent -or $parent -eq $dir) { break }
|
||||||
|
$dir = $parent
|
||||||
|
}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Assert-ExtraArgs {
|
||||||
|
# The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
# output key fails with an opaque 1C error — reject what the skill owns itself.
|
||||||
|
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
|
||||||
|
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
|
||||||
|
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
|
||||||
|
foreach ($tok in $ExtraArgs) {
|
||||||
|
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
|
||||||
|
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
foreach ($k in $owned) {
|
||||||
|
if (Test-ArgKeyMatch $tok $k) {
|
||||||
|
$hint = ''
|
||||||
|
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
|
||||||
|
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-ExtraArgs {
|
||||||
|
# Pick the argument list for the selected engine and validate it. An explicitly passed
|
||||||
|
# parameter for the other engine is an error; the same keys coming from .v8-project.json
|
||||||
|
# simply do not apply — a project may describe both engines.
|
||||||
|
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
|
||||||
|
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
|
||||||
|
# space-separated values spill into positional ones, a comma-joined list arrives as a
|
||||||
|
# single token. So accept the repo's list convention (comma-separated) and split here;
|
||||||
|
# a native array call keeps working. A value containing a comma is not supported.
|
||||||
|
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -eq 'ibcmd') {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
|
||||||
|
} else {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
|
||||||
|
}
|
||||||
|
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
|
||||||
|
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
|
||||||
|
# would nest the array — the tokens would then be glued into one argument.
|
||||||
|
return $extra
|
||||||
|
}
|
||||||
|
|
||||||
|
function Format-ArgsForDisplay {
|
||||||
|
# Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
# a leaked password does.
|
||||||
|
param([string[]]$ArgList, [string]$Engine)
|
||||||
|
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
|
||||||
|
$res = @()
|
||||||
|
$maskNext = $false
|
||||||
|
foreach ($tok in $ArgList) {
|
||||||
|
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
|
||||||
|
$hit = $null
|
||||||
|
foreach ($k in $keys) {
|
||||||
|
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
|
||||||
|
}
|
||||||
|
if (-not $hit) { $res += $tok; continue }
|
||||||
|
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
|
||||||
|
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
|
||||||
|
else { $res += ($hit + '***') }
|
||||||
|
}
|
||||||
|
return ,$res
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-CleanPath {
|
||||||
|
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
# inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
|
||||||
|
param([string]$Value, [string]$ParamName)
|
||||||
|
if (-not $Value) { return $Value }
|
||||||
|
$v = $Value.Trim()
|
||||||
|
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
|
||||||
|
$v = $v.Substring(1, $v.Length - 2).Trim()
|
||||||
|
}
|
||||||
|
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
|
||||||
|
if ($v.Contains('"')) {
|
||||||
|
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
return $v
|
||||||
|
}
|
||||||
|
|
||||||
|
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
|
||||||
|
$InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath'
|
||||||
|
$ConfigDir = ConvertTo-CleanPath $ConfigDir '-ConfigDir'
|
||||||
|
|
||||||
|
function Assert-InfoBaseExists {
|
||||||
|
# These skills work on a ready infobase. Saying so up front beats the platform's
|
||||||
|
# "Неверные или отсутствующие параметры соединения" after a launch.
|
||||||
|
param([string]$Path)
|
||||||
|
if (-not $Path) { return }
|
||||||
|
if (-not (Test-Path (Join-Path $Path "1Cv8.1CD"))) {
|
||||||
|
Write-Host "Error: information base not found at $Path (no 1Cv8.1CD)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert-InfoBaseExists $InfoBasePath
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
function Find-ProjectV8Path {
|
||||||
$dir = (Get-Location).Path
|
$dir = (Get-Location).Path
|
||||||
@@ -128,7 +306,7 @@ if (-not $V8Path) {
|
|||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -137,13 +315,94 @@ if (Test-Path $V8Path -PathType Container) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
|
function ConvertFrom-PlatformBytes {
|
||||||
|
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
|
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
|
||||||
|
# one of them outright mangles Cyrillic.
|
||||||
|
param([byte[]]$Bytes)
|
||||||
|
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
|
||||||
|
try {
|
||||||
|
$strict = New-Object System.Text.UTF8Encoding($false, $true)
|
||||||
|
return $strict.GetString($Bytes)
|
||||||
|
} catch {
|
||||||
|
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-PlatformProcess {
|
||||||
|
# Run the platform non-interactively and capture its console output. A closed stdin pipe
|
||||||
|
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
|
||||||
|
# text out of our stream until we print it labelled (and out of the wrong encoding).
|
||||||
|
# Returns @{ Output; ExitCode }.
|
||||||
|
#
|
||||||
|
# Quoting differs by engine, so the caller says which it built:
|
||||||
|
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
|
||||||
|
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
|
||||||
|
# which is where 1C's own parser expects them; quoting again breaks the value.
|
||||||
|
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = if ($PreQuoted) {
|
||||||
|
$ProcArgs -join ' '
|
||||||
|
} else {
|
||||||
|
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
}
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
# stderr is drained in parallel: reading the streams one after another deadlocks
|
||||||
|
# as soon as the other one fills its pipe buffer.
|
||||||
|
$errMs = New-Object System.IO.MemoryStream
|
||||||
|
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
|
||||||
|
$outMs = New-Object System.IO.MemoryStream
|
||||||
|
$p.StandardOutput.BaseStream.CopyTo($outMs)
|
||||||
|
$errTask.Wait()
|
||||||
|
$p.WaitForExit()
|
||||||
|
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
|
||||||
|
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-PlatformOutput {
|
||||||
|
# Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
|
||||||
|
param([string]$Text)
|
||||||
|
if (-not $Text) { return }
|
||||||
|
$t = $Text.TrimEnd()
|
||||||
|
if (-not $t) { return }
|
||||||
|
$limit = 65536
|
||||||
|
if ($t.Length -gt $limit) {
|
||||||
|
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
|
||||||
|
}
|
||||||
|
Write-Host "--- Вывод платформы ---"
|
||||||
|
Write-Host $t
|
||||||
|
Write-Host "--- End ---"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function Test-DirNonEmpty {
|
||||||
|
# Postcondition: the platform must have written files into the output directory.
|
||||||
|
# Exit code 0 with an empty dir (broken/headless env) is a false success — reject it.
|
||||||
|
param([string]$Path)
|
||||||
|
return (Test-Path $Path -PathType Container) -and ([bool](Get-ChildItem -LiteralPath $Path -Force -ErrorAction SilentlyContinue | Select-Object -First 1))
|
||||||
|
}
|
||||||
|
|
||||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' }
|
||||||
|
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if ($engine -eq "ibcmd") {
|
if ($engine -eq "ibcmd") {
|
||||||
if (-not $InfoBasePath) {
|
if (-not $InfoBasePath) {
|
||||||
@@ -196,15 +455,21 @@ try {
|
|||||||
if ($UserName) { $arguments += "--user=$UserName" }
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
if ($Password) { $arguments += "--password=$Password" }
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
$arguments += "--data=$tempDir"
|
$arguments += "--data=$tempDir"
|
||||||
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
$arguments += $extraArgs
|
||||||
$output = & $V8Path @arguments 2>&1
|
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
||||||
$exitCode = $LASTEXITCODE
|
$__ib = Invoke-PlatformProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
$outMissing = ($exitCode -eq 0) -and -not (Test-DirNonEmpty $ConfigDir)
|
||||||
|
if ($outMissing) { $exitCode = 1 }
|
||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
Write-Host "Configuration exported successfully to: $ConfigDir" -ForegroundColor Green
|
Write-Host "Configuration exported successfully to: $ConfigDir" -ForegroundColor Green
|
||||||
|
} elseif ($outMissing) {
|
||||||
|
Write-Host "Error: exit code 0 but no files under $ConfigDir — configuration was not exported" -ForegroundColor Red
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error exporting configuration (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error exporting configuration (code: $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
if ($output) { Write-Host ($output | Out-String) }
|
Write-PlatformOutput $output
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,16 +527,22 @@ try {
|
|||||||
$outFile = Join-Path $tempDir "dump_log.txt"
|
$outFile = Join-Path $tempDir "dump_log.txt"
|
||||||
$arguments += "/Out", "`"$outFile`""
|
$arguments += "/Out", "`"$outFile`""
|
||||||
$arguments += "/DisableStartupDialogs"
|
$arguments += "/DisableStartupDialogs"
|
||||||
|
$arguments += $extraArgs
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
|
||||||
$exitCode = $process.ExitCode
|
$exitCode = $__v8.ExitCode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
|
# Postcondition: exit 0 with an empty output directory is a false success.
|
||||||
|
$outMissing = ($exitCode -eq 0) -and -not (Test-DirNonEmpty $ConfigDir)
|
||||||
|
if ($outMissing) { $exitCode = 1 }
|
||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
Write-Host "Dump completed successfully" -ForegroundColor Green
|
Write-Host "Dump completed successfully" -ForegroundColor Green
|
||||||
Write-Host "Configuration dumped to: $ConfigDir"
|
Write-Host "Configuration dumped to: $ConfigDir"
|
||||||
|
} elseif ($outMissing) {
|
||||||
|
Write-Host "Error: exit code 0 but no files under $ConfigDir — configuration was not dumped" -ForegroundColor Red
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
@@ -284,6 +555,7 @@ try {
|
|||||||
Write-Host "--- End ---"
|
Write-Host "--- End ---"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Write-PlatformOutput $__v8.Output
|
||||||
|
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-dump-xml v1.6 — Dump 1C configuration to XML files
|
# db-dump-xml v1.14 — 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
|
||||||
@@ -36,36 +36,323 @@ def _find_project_v8path():
|
|||||||
d = parent
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
V8_OWNED_KEYS = [
|
||||||
|
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
|
||||||
|
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
|
||||||
|
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
|
||||||
|
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
|
||||||
|
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||||
|
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||||
|
]
|
||||||
|
IBCMD_OWNED_KEYS = [
|
||||||
|
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||||
|
"--import", "--export", "--apply", "--force", "--create-database",
|
||||||
|
"--user", "--password",
|
||||||
|
]
|
||||||
|
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
|
||||||
|
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
|
||||||
|
|
||||||
|
|
||||||
|
def arg_key_match(token, key):
|
||||||
|
"""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
|
||||||
|
/ClearCache distinct from /C."""
|
||||||
|
if len(token) < len(key):
|
||||||
|
return False
|
||||||
|
if token[: len(key)].lower() != key.lower():
|
||||||
|
return False
|
||||||
|
if len(token) == len(key):
|
||||||
|
return True
|
||||||
|
return not token[len(key)].isalpha()
|
||||||
|
|
||||||
|
|
||||||
|
def project_extra_args(name):
|
||||||
|
"""v8args / ibcmdargs from .v8-project.json — same upward walk as v8path."""
|
||||||
|
d = os.getcwd()
|
||||||
|
while True:
|
||||||
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pf):
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
v = data.get(name)
|
||||||
|
if v:
|
||||||
|
return [str(x) for x in v]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return []
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return []
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def assert_extra_args(extra, engine, hints):
|
||||||
|
"""The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
output key fails with an opaque 1C error — reject what the skill owns itself."""
|
||||||
|
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
|
||||||
|
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
|
||||||
|
for tok in extra:
|
||||||
|
if engine == "ibcmd" and not tok.startswith("-"):
|
||||||
|
print(
|
||||||
|
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||||
|
f"({param} cannot extend the ibcmd command)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
for k in owned:
|
||||||
|
if arg_key_match(tok, k):
|
||||||
|
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||||
|
print(
|
||||||
|
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def format_args_for_display(arglist, engine):
|
||||||
|
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
a leaked password does."""
|
||||||
|
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
|
||||||
|
res = []
|
||||||
|
mask_next = False
|
||||||
|
for tok in arglist:
|
||||||
|
if mask_next:
|
||||||
|
res.append("***")
|
||||||
|
mask_next = False
|
||||||
|
continue
|
||||||
|
hit = None
|
||||||
|
for k in keys:
|
||||||
|
if tok[: len(k)].lower() == k.lower():
|
||||||
|
hit = k
|
||||||
|
break
|
||||||
|
if hit is None:
|
||||||
|
res.append(tok)
|
||||||
|
elif len(tok) == len(hit):
|
||||||
|
res.append(tok)
|
||||||
|
mask_next = True
|
||||||
|
elif tok[len(hit)] == "=":
|
||||||
|
res.append(hit + "=***")
|
||||||
|
else:
|
||||||
|
res.append(hit + "***")
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def extract_extra_args(argv, known_opts):
|
||||||
|
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
|
||||||
|
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
|
||||||
|
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
|
||||||
|
rest, v8, ibcmd = [], [], []
|
||||||
|
i = 0
|
||||||
|
while i < len(argv):
|
||||||
|
low = argv[i].lower()
|
||||||
|
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
|
||||||
|
target = v8 if low == "-additionalv8arguments" else ibcmd
|
||||||
|
i += 1
|
||||||
|
while i < len(argv) and argv[i].lower() not in known_opts:
|
||||||
|
target.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
rest.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
return rest, v8, ibcmd
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
||||||
|
"""Pick the argument list for the selected engine and validate it. An explicitly
|
||||||
|
passed parameter for the other engine is an error; the same keys coming from
|
||||||
|
.v8-project.json simply do not apply — a project may describe both engines.
|
||||||
|
|
||||||
|
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
|
||||||
|
so that form is the documented one and both ports must accept it. A value containing
|
||||||
|
a comma is not supported."""
|
||||||
|
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
|
||||||
|
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
|
||||||
|
if engine == "ibcmd" and v8_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||||
|
"(use -AdditionalIbcmdArguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine != "ibcmd" and ibcmd_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||||
|
"(use -AdditionalV8Arguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine == "ibcmd":
|
||||||
|
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
|
||||||
|
else:
|
||||||
|
extra = project_extra_args("v8args") + list(v8_extra)
|
||||||
|
if extra:
|
||||||
|
assert_extra_args(extra, engine, hints)
|
||||||
|
return extra
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
def _version_key(p):
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
"""Numeric sort key from version dir name."""
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def resolve_v8path(v8path):
|
||||||
"""Resolve path to 1cv8.exe."""
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
if not v8path:
|
if not v8path:
|
||||||
v8path = _find_project_v8path()
|
v8path = _find_project_v8path()
|
||||||
if not v8path:
|
if not v8path:
|
||||||
candidates = (
|
if os.name == "nt":
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
candidates = (
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
)
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
if candidates:
|
if candidates:
|
||||||
v8path = max(candidates, key=_version_key)
|
v8path = max(candidates, key=_version_key)
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
else:
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
if os.path.isdir(v8path):
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def decode_platform_bytes(data):
|
||||||
|
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
|
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — the locale
|
||||||
|
code page (what text=True uses) mangles both."""
|
||||||
|
if not data:
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
return data.decode("utf-8")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
return data.decode("cp866", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def assert_infobase_exists(path):
|
||||||
|
"""These skills work on a ready infobase. Saying so up front beats the platform's
|
||||||
|
"Неверные или отсутствующие параметры соединения" after a launch."""
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
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)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def clean_path(value, param=""):
|
||||||
|
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
|
||||||
|
if not value:
|
||||||
|
return value
|
||||||
|
v = value.strip()
|
||||||
|
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
|
||||||
|
v = v[1:-1].strip()
|
||||||
|
if len(v) > 3 and v[-1] in "\\/":
|
||||||
|
v = v[:-1]
|
||||||
|
if '"' in v:
|
||||||
|
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
def quote_if_needed(token):
|
||||||
|
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
|
||||||
|
verbatim, so a token with a space needs quotes of its own."""
|
||||||
|
if token and (" " in token or "\t" in token) and '"' not in token:
|
||||||
|
return f'"{token}"'
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
def run_v8(v8path, arguments):
|
||||||
|
"""Run 1cv8 in batch mode and capture its console output.
|
||||||
|
|
||||||
|
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
|
||||||
|
escape those quotes, so there the command line is handed over ready-made.
|
||||||
|
"""
|
||||||
|
if os.name == "nt":
|
||||||
|
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||||
|
else:
|
||||||
|
cmd = [v8path] + arguments
|
||||||
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def print_platform_output(result):
|
||||||
|
"""Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
|
||||||
|
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
|
||||||
|
if not text:
|
||||||
|
return
|
||||||
|
limit = 65536
|
||||||
|
if len(text) > limit:
|
||||||
|
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
|
||||||
|
print("--- Вывод платформы ---")
|
||||||
|
print(text)
|
||||||
|
print("--- End ---")
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def dir_nonempty(path):
|
||||||
|
"""Postcondition: the platform must have written files into the output directory.
|
||||||
|
Exit code 0 with an empty dir (broken/headless env) is a false success — reject it."""
|
||||||
|
return os.path.isdir(path) and any(os.scandir(path))
|
||||||
|
|
||||||
|
|
||||||
|
def _redact(text, *secrets):
|
||||||
|
"""Redact literal secret values (password, user) from a display string —
|
||||||
|
precise, never touches lookalike paths."""
|
||||||
|
for s in secrets:
|
||||||
|
if s:
|
||||||
|
text = text.replace(s, "***")
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
@@ -95,12 +382,35 @@ def main():
|
|||||||
choices=["Hierarchical", "Plain"],
|
choices=["Hierarchical", "Plain"],
|
||||||
help="Dump format (default: Hierarchical)",
|
help="Dump format (default: Hierarchical)",
|
||||||
)
|
)
|
||||||
args = parser.parse_args()
|
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
|
||||||
|
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
|
||||||
|
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
|
||||||
|
help="Extra ibcmd arguments in --key=value form")
|
||||||
|
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
||||||
|
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
||||||
|
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
||||||
|
assert_infobase_exists(args.InfoBasePath)
|
||||||
|
args.ConfigDir = clean_path(args.ConfigDir, "-ConfigDir")
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
arg_hints = {
|
||||||
|
"/F": "-InfoBasePath",
|
||||||
|
"/S": "-InfoBaseServer + -InfoBaseRef",
|
||||||
|
"/N": "-UserName",
|
||||||
|
"/P": "-Password",
|
||||||
|
"--db-path": "-InfoBasePath",
|
||||||
|
"--user": "-UserName",
|
||||||
|
"--password": "-Password",
|
||||||
|
}
|
||||||
|
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if engine == "ibcmd":
|
if engine == "ibcmd":
|
||||||
if not args.InfoBasePath:
|
if not args.InfoBasePath:
|
||||||
@@ -148,17 +458,20 @@ def main():
|
|||||||
if args.Password:
|
if args.Password:
|
||||||
arguments.append(f"--password={args.Password}")
|
arguments.append(f"--password={args.Password}")
|
||||||
arguments.append(f"--data={ib_data}")
|
arguments.append(f"--data={ib_data}")
|
||||||
print(f"Running: ibcmd {' '.join(arguments)}")
|
arguments.extend(extra_args)
|
||||||
result = subprocess.run([v8path] + arguments, capture_output=True, encoding="utf-8", errors="replace")
|
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||||
if result.returncode == 0:
|
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||||
|
exit_code = result.returncode
|
||||||
|
out_missing = exit_code == 0 and not dir_nonempty(args.ConfigDir)
|
||||||
|
if out_missing:
|
||||||
|
exit_code = 1
|
||||||
|
if exit_code == 0:
|
||||||
print(f"Configuration exported successfully to: {args.ConfigDir}")
|
print(f"Configuration exported successfully to: {args.ConfigDir}")
|
||||||
|
elif out_missing:
|
||||||
|
print(f"Error: exit code 0 but no files under {args.ConfigDir} — configuration was not exported", file=sys.stderr)
|
||||||
else:
|
else:
|
||||||
print(f"Error exporting configuration (code: {result.returncode})", file=sys.stderr)
|
print(f"Error exporting configuration (code: {exit_code})", file=sys.stderr)
|
||||||
if result.stdout:
|
sys.exit(exit_code)
|
||||||
print(result.stdout)
|
|
||||||
if result.stderr:
|
|
||||||
print(result.stderr, file=sys.stderr)
|
|
||||||
sys.exit(result.returncode)
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
# --- Temp dir ---
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_xml_{random.randint(0, 999999)}")
|
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_xml_{random.randint(0, 999999)}")
|
||||||
@@ -169,16 +482,16 @@ def main():
|
|||||||
arguments = ["DESIGNER"]
|
arguments = ["DESIGNER"]
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
if args.InfoBaseServer and args.InfoBaseRef:
|
||||||
arguments += ["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"]
|
arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
|
||||||
else:
|
else:
|
||||||
arguments += ["/F", args.InfoBasePath]
|
arguments += ["/F", f'"{args.InfoBasePath}"']
|
||||||
|
|
||||||
if args.UserName:
|
if args.UserName:
|
||||||
arguments.append(f"/N{args.UserName}")
|
arguments.append(f'/N"{args.UserName}"')
|
||||||
if args.Password:
|
if args.Password:
|
||||||
arguments.append(f"/P{args.Password}")
|
arguments.append(f'/P"{args.Password}"')
|
||||||
|
|
||||||
arguments += ["/DumpConfigToFiles", args.ConfigDir]
|
arguments += ["/DumpConfigToFiles", f'"{args.ConfigDir}"']
|
||||||
arguments += ["-Format", args.Format]
|
arguments += ["-Format", args.Format]
|
||||||
|
|
||||||
if args.Mode == "Full":
|
if args.Mode == "Full":
|
||||||
@@ -195,7 +508,7 @@ def main():
|
|||||||
with open(list_file, "w", encoding="utf-8-sig") as f:
|
with open(list_file, "w", encoding="utf-8-sig") as f:
|
||||||
f.write("\n".join(object_list))
|
f.write("\n".join(object_list))
|
||||||
|
|
||||||
arguments += ["-listFile", list_file]
|
arguments += ["-listFile", f'"{list_file}"']
|
||||||
print(f"Objects to dump: {len(object_list)}")
|
print(f"Objects to dump: {len(object_list)}")
|
||||||
for obj in object_list:
|
for obj in object_list:
|
||||||
print(f" {obj}")
|
print(f" {obj}")
|
||||||
@@ -205,28 +518,31 @@ def main():
|
|||||||
|
|
||||||
# --- Extensions ---
|
# --- Extensions ---
|
||||||
if args.Extension:
|
if args.Extension:
|
||||||
arguments += ["-Extension", args.Extension]
|
arguments += ["-Extension", f'"{args.Extension}"']
|
||||||
elif args.AllExtensions:
|
elif args.AllExtensions:
|
||||||
arguments.append("-AllExtensions")
|
arguments.append("-AllExtensions")
|
||||||
|
|
||||||
# --- Output ---
|
# --- Output ---
|
||||||
out_file = os.path.join(temp_dir, "dump_log.txt")
|
out_file = os.path.join(temp_dir, "dump_log.txt")
|
||||||
arguments += ["/Out", out_file]
|
arguments += ["/Out", f'"{out_file}"']
|
||||||
arguments.append("/DisableStartupDialogs")
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
arguments.extend(quote_if_needed(a) for a in extra_args)
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||||
result = subprocess.run(
|
result = run_v8(v8path, arguments)
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
exit_code = result.returncode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
|
# Postcondition: exit 0 with an empty output directory is a false success.
|
||||||
|
out_missing = exit_code == 0 and not dir_nonempty(args.ConfigDir)
|
||||||
|
if out_missing:
|
||||||
|
exit_code = 1
|
||||||
if exit_code == 0:
|
if exit_code == 0:
|
||||||
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:
|
||||||
|
print(f"Error: exit code 0 but no files under {args.ConfigDir} — configuration was not dumped", file=sys.stderr)
|
||||||
else:
|
else:
|
||||||
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
|
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
|
||||||
|
|
||||||
@@ -241,6 +557,7 @@ def main():
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
print_platform_output(result)
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ allowed-tools:
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"v8path": "C:\\Program Files\\1cv8\\8.3.25.1257\\bin",
|
"v8path": "C:\\Program Files\\1cv8\\8.3.25.1257\\bin",
|
||||||
|
"v8args": ["/UseHwLicenses+"],
|
||||||
"databases": [
|
"databases": [
|
||||||
{
|
{
|
||||||
"id": "dev",
|
"id": "dev",
|
||||||
@@ -61,6 +62,8 @@ allowed-tools:
|
|||||||
| Поле | Тип | Описание |
|
| Поле | Тип | Описание |
|
||||||
|------|-----|----------|
|
|------|-----|----------|
|
||||||
| `v8path` | string | Каталог bin платформы 1С. Необязательный — если не задан, автоопределение |
|
| `v8path` | string | Каталог bin платформы 1С. Необязательный — если не задан, автоопределение |
|
||||||
|
| `v8args` | array | Доп. аргументы запуска `1cv8.exe` для всех навыков, напр. `["/UseHwLicenses+"]` |
|
||||||
|
| `ibcmdargs` | array | Доп. аргументы `ibcmd` (форма `--ключ=значение`) |
|
||||||
| `databases` | array | Массив баз данных |
|
| `databases` | array | Массив баз данных |
|
||||||
| `default` | string | id базы по умолчанию |
|
| `default` | string | id базы по умолчанию |
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" <п
|
|||||||
| `-InputFile <путь>` | да | Путь к CF-файлу |
|
| `-InputFile <путь>` | да | Путь к CF-файлу |
|
||||||
| `-Extension <имя>` | нет | Загрузить как расширение |
|
| `-Extension <имя>` | нет | Загрузить как расширение |
|
||||||
| `-AllExtensions` | нет | Загрузить все расширения из архива |
|
| `-AllExtensions` | нет | Загрузить все расширения из архива |
|
||||||
|
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
|
||||||
|
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
|
||||||
|
|
||||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# db-load-cf v1.4 — Load 1C configuration from CF file
|
# db-load-cf v1.13 — Load 1C configuration from CF file
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Загрузка конфигурации 1С из CF-файла
|
Загрузка конфигурации 1С из CF-файла
|
||||||
@@ -35,6 +36,12 @@
|
|||||||
.PARAMETER AllExtensions
|
.PARAMETER AllExtensions
|
||||||
Загрузить все расширения из архива
|
Загрузить все расширения из архива
|
||||||
|
|
||||||
|
.PARAMETER AdditionalV8Arguments
|
||||||
|
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
|
||||||
|
|
||||||
|
.PARAMETER AdditionalIbcmdArguments
|
||||||
|
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
|
||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
.\db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "config.cf"
|
.\db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "config.cf"
|
||||||
|
|
||||||
@@ -69,12 +76,200 @@ param(
|
|||||||
[string]$Extension,
|
[string]$Extension,
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[switch]$AllExtensions
|
[switch]$AllExtensions,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalV8Arguments = @(),
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalIbcmdArguments = @()
|
||||||
)
|
)
|
||||||
|
|
||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
function Protect-Secrets {
|
||||||
|
# Redact literal secret values from a display string (String.Replace is literal, not regex).
|
||||||
|
param([string]$Text, [string[]]$Secrets)
|
||||||
|
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
|
||||||
|
return $Text
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ExitAnnotation {
|
||||||
|
# Annotate an abnormal process exit code so a crash isn't reported as a bare number.
|
||||||
|
# A batch DESIGNER that crashes (e.g. missing license) may leave the infobase locked or
|
||||||
|
# half-updated — surface that instead of a plain code. (Windows exception codes only;
|
||||||
|
# POSIX signals are handled in the .py port.)
|
||||||
|
param([int]$Code)
|
||||||
|
$win = @{
|
||||||
|
-1073741819 = "0xC0000005 (access violation)"
|
||||||
|
-1073741515 = "0xC0000135 (missing DLL)"
|
||||||
|
-1073740791 = "0xC0000409 (stack overrun)"
|
||||||
|
}
|
||||||
|
if ($win.ContainsKey($Code)) {
|
||||||
|
return " — abnormal termination, exception $($win[$Code]); the infobase may be left in an inconsistent state; verify it before retrying"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
$script:V8OwnedKeys = @(
|
||||||
|
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
|
||||||
|
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
|
||||||
|
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
|
||||||
|
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
|
||||||
|
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||||
|
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||||
|
)
|
||||||
|
$script:IbcmdOwnedKeys = @(
|
||||||
|
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||||
|
'--import', '--export', '--apply', '--force', '--create-database',
|
||||||
|
'--user', '--password'
|
||||||
|
)
|
||||||
|
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
|
||||||
|
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
|
||||||
|
|
||||||
|
function Test-ArgKeyMatch {
|
||||||
|
# A token matches a key when it equals the key, or starts with it and the next
|
||||||
|
# character is not a letter — catches glued /N"user" and --password=x, while
|
||||||
|
# keeping /ClearCache distinct from /C.
|
||||||
|
param([string]$Token, [string]$Key)
|
||||||
|
if ($Token.Length -lt $Key.Length) { return $false }
|
||||||
|
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
|
||||||
|
if ($Token.Length -eq $Key.Length) { return $true }
|
||||||
|
return -not [char]::IsLetter($Token[$Key.Length])
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ProjectExtraArgs {
|
||||||
|
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
|
||||||
|
param([string]$Name)
|
||||||
|
$dir = (Get-Location).Path
|
||||||
|
while ($dir) {
|
||||||
|
$pf = Join-Path $dir ".v8-project.json"
|
||||||
|
if (Test-Path $pf) {
|
||||||
|
try {
|
||||||
|
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
|
||||||
|
} catch {}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
$parent = Split-Path $dir -Parent
|
||||||
|
if (-not $parent -or $parent -eq $dir) { break }
|
||||||
|
$dir = $parent
|
||||||
|
}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Assert-ExtraArgs {
|
||||||
|
# The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
# output key fails with an opaque 1C error — reject what the skill owns itself.
|
||||||
|
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
|
||||||
|
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
|
||||||
|
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
|
||||||
|
foreach ($tok in $ExtraArgs) {
|
||||||
|
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
|
||||||
|
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
foreach ($k in $owned) {
|
||||||
|
if (Test-ArgKeyMatch $tok $k) {
|
||||||
|
$hint = ''
|
||||||
|
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
|
||||||
|
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-ExtraArgs {
|
||||||
|
# Pick the argument list for the selected engine and validate it. An explicitly passed
|
||||||
|
# parameter for the other engine is an error; the same keys coming from .v8-project.json
|
||||||
|
# simply do not apply — a project may describe both engines.
|
||||||
|
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
|
||||||
|
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
|
||||||
|
# space-separated values spill into positional ones, a comma-joined list arrives as a
|
||||||
|
# single token. So accept the repo's list convention (comma-separated) and split here;
|
||||||
|
# a native array call keeps working. A value containing a comma is not supported.
|
||||||
|
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -eq 'ibcmd') {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
|
||||||
|
} else {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
|
||||||
|
}
|
||||||
|
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
|
||||||
|
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
|
||||||
|
# would nest the array — the tokens would then be glued into one argument.
|
||||||
|
return $extra
|
||||||
|
}
|
||||||
|
|
||||||
|
function Format-ArgsForDisplay {
|
||||||
|
# Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
# a leaked password does.
|
||||||
|
param([string[]]$ArgList, [string]$Engine)
|
||||||
|
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
|
||||||
|
$res = @()
|
||||||
|
$maskNext = $false
|
||||||
|
foreach ($tok in $ArgList) {
|
||||||
|
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
|
||||||
|
$hit = $null
|
||||||
|
foreach ($k in $keys) {
|
||||||
|
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
|
||||||
|
}
|
||||||
|
if (-not $hit) { $res += $tok; continue }
|
||||||
|
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
|
||||||
|
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
|
||||||
|
else { $res += ($hit + '***') }
|
||||||
|
}
|
||||||
|
return ,$res
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-CleanPath {
|
||||||
|
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
# inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
|
||||||
|
param([string]$Value, [string]$ParamName)
|
||||||
|
if (-not $Value) { return $Value }
|
||||||
|
$v = $Value.Trim()
|
||||||
|
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
|
||||||
|
$v = $v.Substring(1, $v.Length - 2).Trim()
|
||||||
|
}
|
||||||
|
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
|
||||||
|
if ($v.Contains('"')) {
|
||||||
|
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
return $v
|
||||||
|
}
|
||||||
|
|
||||||
|
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
|
||||||
|
$InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath'
|
||||||
|
$InputFile = ConvertTo-CleanPath $InputFile '-InputFile'
|
||||||
|
|
||||||
|
function Assert-InfoBaseExists {
|
||||||
|
# These skills work on a ready infobase. Saying so up front beats the platform's
|
||||||
|
# "Неверные или отсутствующие параметры соединения" after a launch.
|
||||||
|
param([string]$Path)
|
||||||
|
if (-not $Path) { return }
|
||||||
|
if (-not (Test-Path (Join-Path $Path "1Cv8.1CD"))) {
|
||||||
|
Write-Host "Error: information base not found at $Path (no 1Cv8.1CD)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert-InfoBaseExists $InfoBasePath
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
function Find-ProjectV8Path {
|
||||||
$dir = (Get-Location).Path
|
$dir = (Get-Location).Path
|
||||||
@@ -105,7 +300,7 @@ if (-not $V8Path) {
|
|||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,13 +309,87 @@ if (Test-Path $V8Path -PathType Container) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
|
function ConvertFrom-PlatformBytes {
|
||||||
|
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
|
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
|
||||||
|
# one of them outright mangles Cyrillic.
|
||||||
|
param([byte[]]$Bytes)
|
||||||
|
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
|
||||||
|
try {
|
||||||
|
$strict = New-Object System.Text.UTF8Encoding($false, $true)
|
||||||
|
return $strict.GetString($Bytes)
|
||||||
|
} catch {
|
||||||
|
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-PlatformProcess {
|
||||||
|
# Run the platform non-interactively and capture its console output. A closed stdin pipe
|
||||||
|
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
|
||||||
|
# text out of our stream until we print it labelled (and out of the wrong encoding).
|
||||||
|
# Returns @{ Output; ExitCode }.
|
||||||
|
#
|
||||||
|
# Quoting differs by engine, so the caller says which it built:
|
||||||
|
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
|
||||||
|
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
|
||||||
|
# which is where 1C's own parser expects them; quoting again breaks the value.
|
||||||
|
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = if ($PreQuoted) {
|
||||||
|
$ProcArgs -join ' '
|
||||||
|
} else {
|
||||||
|
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
}
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
# stderr is drained in parallel: reading the streams one after another deadlocks
|
||||||
|
# as soon as the other one fills its pipe buffer.
|
||||||
|
$errMs = New-Object System.IO.MemoryStream
|
||||||
|
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
|
||||||
|
$outMs = New-Object System.IO.MemoryStream
|
||||||
|
$p.StandardOutput.BaseStream.CopyTo($outMs)
|
||||||
|
$errTask.Wait()
|
||||||
|
$p.WaitForExit()
|
||||||
|
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
|
||||||
|
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-PlatformOutput {
|
||||||
|
# Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
|
||||||
|
param([string]$Text)
|
||||||
|
if (-not $Text) { return }
|
||||||
|
$t = $Text.TrimEnd()
|
||||||
|
if (-not $t) { return }
|
||||||
|
$limit = 65536
|
||||||
|
if ($t.Length -gt $limit) {
|
||||||
|
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
|
||||||
|
}
|
||||||
|
Write-Host "--- Вывод платформы ---"
|
||||||
|
Write-Host $t
|
||||||
|
Write-Host "--- End ---"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' }
|
||||||
|
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if ($engine -eq "ibcmd") {
|
if ($engine -eq "ibcmd") {
|
||||||
if (-not $InfoBasePath) {
|
if (-not $InfoBasePath) {
|
||||||
@@ -155,15 +424,17 @@ try {
|
|||||||
if ($UserName) { $arguments += "--user=$UserName" }
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
if ($Password) { $arguments += "--password=$Password" }
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
$arguments += "--data=$tempDir"
|
$arguments += "--data=$tempDir"
|
||||||
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
$arguments += $extraArgs
|
||||||
$output = & $V8Path @arguments 2>&1
|
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
||||||
$exitCode = $LASTEXITCODE
|
$__ib = Invoke-PlatformProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
Write-Host "Configuration loaded successfully from: $InputFile" -ForegroundColor Green
|
Write-Host "Configuration loaded successfully from: $InputFile" -ForegroundColor Green
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error loading configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
if ($output) { Write-Host ($output | Out-String) }
|
Write-PlatformOutput $output
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,17 +464,18 @@ try {
|
|||||||
$outFile = Join-Path $tempDir "load_cf_log.txt"
|
$outFile = Join-Path $tempDir "load_cf_log.txt"
|
||||||
$arguments += "/Out", "`"$outFile`""
|
$arguments += "/Out", "`"$outFile`""
|
||||||
$arguments += "/DisableStartupDialogs"
|
$arguments += "/DisableStartupDialogs"
|
||||||
|
$arguments += $extraArgs
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
|
||||||
$exitCode = $process.ExitCode
|
$exitCode = $__v8.ExitCode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
Write-Host "Configuration loaded successfully from: $InputFile" -ForegroundColor Green
|
Write-Host "Configuration loaded successfully from: $InputFile" -ForegroundColor Green
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error loading configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Test-Path $outFile) {
|
if (Test-Path $outFile) {
|
||||||
@@ -214,6 +486,7 @@ try {
|
|||||||
Write-Host "--- End ---"
|
Write-Host "--- End ---"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Write-PlatformOutput $__v8.Output
|
||||||
|
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-load-cf v1.4 — Load 1C configuration from CF file
|
# db-load-cf v1.13 — 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
|
||||||
@@ -36,36 +36,341 @@ def _find_project_v8path():
|
|||||||
d = parent
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
V8_OWNED_KEYS = [
|
||||||
|
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
|
||||||
|
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
|
||||||
|
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
|
||||||
|
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
|
||||||
|
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||||
|
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||||
|
]
|
||||||
|
IBCMD_OWNED_KEYS = [
|
||||||
|
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||||
|
"--import", "--export", "--apply", "--force", "--create-database",
|
||||||
|
"--user", "--password",
|
||||||
|
]
|
||||||
|
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
|
||||||
|
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
|
||||||
|
|
||||||
|
|
||||||
|
def arg_key_match(token, key):
|
||||||
|
"""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
|
||||||
|
/ClearCache distinct from /C."""
|
||||||
|
if len(token) < len(key):
|
||||||
|
return False
|
||||||
|
if token[: len(key)].lower() != key.lower():
|
||||||
|
return False
|
||||||
|
if len(token) == len(key):
|
||||||
|
return True
|
||||||
|
return not token[len(key)].isalpha()
|
||||||
|
|
||||||
|
|
||||||
|
def project_extra_args(name):
|
||||||
|
"""v8args / ibcmdargs from .v8-project.json — same upward walk as v8path."""
|
||||||
|
d = os.getcwd()
|
||||||
|
while True:
|
||||||
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pf):
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
v = data.get(name)
|
||||||
|
if v:
|
||||||
|
return [str(x) for x in v]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return []
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return []
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def assert_extra_args(extra, engine, hints):
|
||||||
|
"""The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
output key fails with an opaque 1C error — reject what the skill owns itself."""
|
||||||
|
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
|
||||||
|
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
|
||||||
|
for tok in extra:
|
||||||
|
if engine == "ibcmd" and not tok.startswith("-"):
|
||||||
|
print(
|
||||||
|
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||||
|
f"({param} cannot extend the ibcmd command)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
for k in owned:
|
||||||
|
if arg_key_match(tok, k):
|
||||||
|
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||||
|
print(
|
||||||
|
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def format_args_for_display(arglist, engine):
|
||||||
|
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
a leaked password does."""
|
||||||
|
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
|
||||||
|
res = []
|
||||||
|
mask_next = False
|
||||||
|
for tok in arglist:
|
||||||
|
if mask_next:
|
||||||
|
res.append("***")
|
||||||
|
mask_next = False
|
||||||
|
continue
|
||||||
|
hit = None
|
||||||
|
for k in keys:
|
||||||
|
if tok[: len(k)].lower() == k.lower():
|
||||||
|
hit = k
|
||||||
|
break
|
||||||
|
if hit is None:
|
||||||
|
res.append(tok)
|
||||||
|
elif len(tok) == len(hit):
|
||||||
|
res.append(tok)
|
||||||
|
mask_next = True
|
||||||
|
elif tok[len(hit)] == "=":
|
||||||
|
res.append(hit + "=***")
|
||||||
|
else:
|
||||||
|
res.append(hit + "***")
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def extract_extra_args(argv, known_opts):
|
||||||
|
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
|
||||||
|
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
|
||||||
|
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
|
||||||
|
rest, v8, ibcmd = [], [], []
|
||||||
|
i = 0
|
||||||
|
while i < len(argv):
|
||||||
|
low = argv[i].lower()
|
||||||
|
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
|
||||||
|
target = v8 if low == "-additionalv8arguments" else ibcmd
|
||||||
|
i += 1
|
||||||
|
while i < len(argv) and argv[i].lower() not in known_opts:
|
||||||
|
target.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
rest.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
return rest, v8, ibcmd
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
||||||
|
"""Pick the argument list for the selected engine and validate it. An explicitly
|
||||||
|
passed parameter for the other engine is an error; the same keys coming from
|
||||||
|
.v8-project.json simply do not apply — a project may describe both engines.
|
||||||
|
|
||||||
|
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
|
||||||
|
so that form is the documented one and both ports must accept it. A value containing
|
||||||
|
a comma is not supported."""
|
||||||
|
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
|
||||||
|
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
|
||||||
|
if engine == "ibcmd" and v8_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||||
|
"(use -AdditionalIbcmdArguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine != "ibcmd" and ibcmd_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||||
|
"(use -AdditionalV8Arguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine == "ibcmd":
|
||||||
|
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
|
||||||
|
else:
|
||||||
|
extra = project_extra_args("v8args") + list(v8_extra)
|
||||||
|
if extra:
|
||||||
|
assert_extra_args(extra, engine, hints)
|
||||||
|
return extra
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
def _version_key(p):
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
"""Numeric sort key from version dir name."""
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def resolve_v8path(v8path):
|
||||||
"""Resolve path to 1cv8.exe."""
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
if not v8path:
|
if not v8path:
|
||||||
v8path = _find_project_v8path()
|
v8path = _find_project_v8path()
|
||||||
if not v8path:
|
if not v8path:
|
||||||
candidates = (
|
if os.name == "nt":
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
candidates = (
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
)
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
if candidates:
|
if candidates:
|
||||||
v8path = max(candidates, key=_version_key)
|
v8path = max(candidates, key=_version_key)
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
else:
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
if os.path.isdir(v8path):
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def decode_platform_bytes(data):
|
||||||
|
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
|
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — the locale
|
||||||
|
code page (what text=True uses) mangles both."""
|
||||||
|
if not data:
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
return data.decode("utf-8")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
return data.decode("cp866", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def assert_infobase_exists(path):
|
||||||
|
"""These skills work on a ready infobase. Saying so up front beats the platform's
|
||||||
|
"Неверные или отсутствующие параметры соединения" after a launch."""
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
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)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def clean_path(value, param=""):
|
||||||
|
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
|
||||||
|
if not value:
|
||||||
|
return value
|
||||||
|
v = value.strip()
|
||||||
|
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
|
||||||
|
v = v[1:-1].strip()
|
||||||
|
if len(v) > 3 and v[-1] in "\\/":
|
||||||
|
v = v[:-1]
|
||||||
|
if '"' in v:
|
||||||
|
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
def quote_if_needed(token):
|
||||||
|
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
|
||||||
|
verbatim, so a token with a space needs quotes of its own."""
|
||||||
|
if token and (" " in token or "\t" in token) and '"' not in token:
|
||||||
|
return f'"{token}"'
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
def run_v8(v8path, arguments):
|
||||||
|
"""Run 1cv8 in batch mode and capture its console output.
|
||||||
|
|
||||||
|
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
|
||||||
|
escape those quotes, so there the command line is handed over ready-made.
|
||||||
|
"""
|
||||||
|
if os.name == "nt":
|
||||||
|
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||||
|
else:
|
||||||
|
cmd = [v8path] + arguments
|
||||||
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def print_platform_output(result):
|
||||||
|
"""Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
|
||||||
|
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
|
||||||
|
if not text:
|
||||||
|
return
|
||||||
|
limit = 65536
|
||||||
|
if len(text) > limit:
|
||||||
|
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
|
||||||
|
print("--- Вывод платформы ---")
|
||||||
|
print(text)
|
||||||
|
print("--- End ---")
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def describe_exit(code):
|
||||||
|
"""Annotate an abnormal process exit code so a crash isn't reported as a bare number.
|
||||||
|
Batch 1C in a broken/headless environment (no GUI session, no license) can crash mid-run
|
||||||
|
instead of returning a clean error, possibly leaving the infobase locked or half-mutated."""
|
||||||
|
if code is None:
|
||||||
|
return ""
|
||||||
|
win = {
|
||||||
|
3221225477: "0xC0000005 (access violation)", -1073741819: "0xC0000005 (access violation)",
|
||||||
|
3221225781: "0xC0000135 (missing DLL)", -1073741515: "0xC0000135 (missing DLL)",
|
||||||
|
3221226505: "0xC0000409 (stack overrun)", -1073740791: "0xC0000409 (stack overrun)",
|
||||||
|
}
|
||||||
|
if code in win:
|
||||||
|
return f" — abnormal termination, exception {win[code]}; the infobase may be left in an inconsistent state; verify it before retrying"
|
||||||
|
if -64 <= code < 0:
|
||||||
|
try:
|
||||||
|
import signal
|
||||||
|
name = signal.Signals(-code).name
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
name = f"signal {-code}"
|
||||||
|
return (f" — process terminated by {name} (abnormal termination, not a normal exit); "
|
||||||
|
"the infobase may be left in an inconsistent state; verify it before retrying")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _redact(text, *secrets):
|
||||||
|
"""Redact literal secret values (password, user) from a display string —
|
||||||
|
precise, never touches lookalike paths."""
|
||||||
|
for s in secrets:
|
||||||
|
if s:
|
||||||
|
text = text.replace(s, "***")
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
@@ -82,11 +387,34 @@ def main():
|
|||||||
parser.add_argument("-InputFile", required=True)
|
parser.add_argument("-InputFile", required=True)
|
||||||
parser.add_argument("-Extension", default="")
|
parser.add_argument("-Extension", default="")
|
||||||
parser.add_argument("-AllExtensions", action="store_true")
|
parser.add_argument("-AllExtensions", action="store_true")
|
||||||
args = parser.parse_args()
|
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
|
||||||
|
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
|
||||||
|
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
|
||||||
|
help="Extra ibcmd arguments in --key=value form")
|
||||||
|
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
||||||
|
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
||||||
|
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
||||||
|
assert_infobase_exists(args.InfoBasePath)
|
||||||
|
args.InputFile = clean_path(args.InputFile, "-InputFile")
|
||||||
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
arg_hints = {
|
||||||
|
"/F": "-InfoBasePath",
|
||||||
|
"/S": "-InfoBaseServer + -InfoBaseRef",
|
||||||
|
"/N": "-UserName",
|
||||||
|
"/P": "-Password",
|
||||||
|
"--db-path": "-InfoBasePath",
|
||||||
|
"--user": "-UserName",
|
||||||
|
"--password": "-Password",
|
||||||
|
}
|
||||||
|
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if engine == "ibcmd":
|
if engine == "ibcmd":
|
||||||
if not args.InfoBasePath:
|
if not args.InfoBasePath:
|
||||||
@@ -117,16 +445,13 @@ def main():
|
|||||||
if args.Password:
|
if args.Password:
|
||||||
arguments.append(f"--password={args.Password}")
|
arguments.append(f"--password={args.Password}")
|
||||||
arguments.append(f"--data={ib_data}")
|
arguments.append(f"--data={ib_data}")
|
||||||
print(f"Running: ibcmd {' '.join(arguments)}")
|
arguments.extend(extra_args)
|
||||||
result = subprocess.run([v8path] + arguments, capture_output=True, encoding="utf-8", errors="replace")
|
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||||
|
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||||
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})", file=sys.stderr)
|
print(f"Error loading configuration (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
|
||||||
if result.stdout:
|
|
||||||
print(result.stdout)
|
|
||||||
if result.stderr:
|
|
||||||
print(result.stderr, file=sys.stderr)
|
|
||||||
sys.exit(result.returncode)
|
sys.exit(result.returncode)
|
||||||
|
|
||||||
# --- Temp dir ---
|
# --- Temp dir ---
|
||||||
@@ -138,42 +463,39 @@ def main():
|
|||||||
arguments = ["DESIGNER"]
|
arguments = ["DESIGNER"]
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
if args.InfoBaseServer and args.InfoBaseRef:
|
||||||
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
|
arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
|
||||||
else:
|
else:
|
||||||
arguments.extend(["/F", args.InfoBasePath])
|
arguments.extend(["/F", f'"{args.InfoBasePath}"'])
|
||||||
|
|
||||||
if args.UserName:
|
if args.UserName:
|
||||||
arguments.append(f"/N{args.UserName}")
|
arguments.append(f'/N"{args.UserName}"')
|
||||||
if args.Password:
|
if args.Password:
|
||||||
arguments.append(f"/P{args.Password}")
|
arguments.append(f'/P"{args.Password}"')
|
||||||
|
|
||||||
arguments.extend(["/LoadCfg", args.InputFile])
|
arguments.extend(["/LoadCfg", f'"{args.InputFile}"'])
|
||||||
|
|
||||||
# --- Extensions ---
|
# --- Extensions ---
|
||||||
if args.Extension:
|
if args.Extension:
|
||||||
arguments.extend(["-Extension", args.Extension])
|
arguments.extend(["-Extension", f'"{args.Extension}"'])
|
||||||
elif args.AllExtensions:
|
elif args.AllExtensions:
|
||||||
arguments.append("-AllExtensions")
|
arguments.append("-AllExtensions")
|
||||||
|
|
||||||
# --- Output ---
|
# --- Output ---
|
||||||
out_file = os.path.join(temp_dir, "load_cf_log.txt")
|
out_file = os.path.join(temp_dir, "load_cf_log.txt")
|
||||||
arguments.extend(["/Out", out_file])
|
arguments.extend(["/Out", f'"{out_file}"'])
|
||||||
arguments.append("/DisableStartupDialogs")
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
arguments.extend(quote_if_needed(a) for a in extra_args)
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||||
result = subprocess.run(
|
result = run_v8(v8path, arguments)
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
exit_code = result.returncode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
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})", file=sys.stderr)
|
print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
|
||||||
|
|
||||||
if os.path.isfile(out_file):
|
if os.path.isfile(out_file):
|
||||||
try:
|
try:
|
||||||
@@ -186,6 +508,7 @@ def main():
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
print_platform_output(result)
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" <п
|
|||||||
| `-InputFile <путь>` | да | Путь к DT-файлу |
|
| `-InputFile <путь>` | да | Путь к DT-файлу |
|
||||||
| `-JobsCount <N>` | нет | Число фоновых заданий загрузки (0 = по числу процессоров) |
|
| `-JobsCount <N>` | нет | Число фоновых заданий загрузки (0 = по числу процессоров) |
|
||||||
| `-UnlockCode <код>` | нет | Код разблокировки (`/UC`), если заблокировано начало сеансов |
|
| `-UnlockCode <код>` | нет | Код разблокировки (`/UC`), если заблокировано начало сеансов |
|
||||||
|
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
|
||||||
|
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
|
||||||
|
|
||||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# db-load-dt v1.3 — Load 1C information base from DT file
|
# db-load-dt v1.12 — 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 не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Загрузка информационной базы 1С из DT-файла
|
Загрузка информационной базы 1С из DT-файла
|
||||||
@@ -35,6 +36,12 @@
|
|||||||
.PARAMETER UnlockCode
|
.PARAMETER UnlockCode
|
||||||
Код разблокировки базы (/UC) — если заблокировано начало сеансов
|
Код разблокировки базы (/UC) — если заблокировано начало сеансов
|
||||||
|
|
||||||
|
.PARAMETER AdditionalV8Arguments
|
||||||
|
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
|
||||||
|
|
||||||
|
.PARAMETER AdditionalIbcmdArguments
|
||||||
|
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
|
||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
.\db-load-dt.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "backup.dt"
|
.\db-load-dt.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "backup.dt"
|
||||||
#>
|
#>
|
||||||
@@ -66,12 +73,200 @@ param(
|
|||||||
[int]$JobsCount = 0,
|
[int]$JobsCount = 0,
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[string]$UnlockCode
|
[string]$UnlockCode,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalV8Arguments = @(),
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalIbcmdArguments = @()
|
||||||
)
|
)
|
||||||
|
|
||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
function Protect-Secrets {
|
||||||
|
# Redact literal secret values from a display string (String.Replace is literal, not regex).
|
||||||
|
param([string]$Text, [string[]]$Secrets)
|
||||||
|
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
|
||||||
|
return $Text
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ExitAnnotation {
|
||||||
|
# Annotate an abnormal process exit code so a crash isn't reported as a bare number.
|
||||||
|
# A batch DESIGNER that crashes (e.g. missing license) may leave the infobase locked or
|
||||||
|
# half-updated — surface that instead of a plain code. (Windows exception codes only;
|
||||||
|
# POSIX signals are handled in the .py port.)
|
||||||
|
param([int]$Code)
|
||||||
|
$win = @{
|
||||||
|
-1073741819 = "0xC0000005 (access violation)"
|
||||||
|
-1073741515 = "0xC0000135 (missing DLL)"
|
||||||
|
-1073740791 = "0xC0000409 (stack overrun)"
|
||||||
|
}
|
||||||
|
if ($win.ContainsKey($Code)) {
|
||||||
|
return " — abnormal termination, exception $($win[$Code]); the infobase may be left in an inconsistent state; verify it before retrying"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
$script:V8OwnedKeys = @(
|
||||||
|
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
|
||||||
|
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
|
||||||
|
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
|
||||||
|
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
|
||||||
|
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||||
|
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||||
|
)
|
||||||
|
$script:IbcmdOwnedKeys = @(
|
||||||
|
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||||
|
'--import', '--export', '--apply', '--force', '--create-database',
|
||||||
|
'--user', '--password'
|
||||||
|
)
|
||||||
|
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
|
||||||
|
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
|
||||||
|
|
||||||
|
function Test-ArgKeyMatch {
|
||||||
|
# A token matches a key when it equals the key, or starts with it and the next
|
||||||
|
# character is not a letter — catches glued /N"user" and --password=x, while
|
||||||
|
# keeping /ClearCache distinct from /C.
|
||||||
|
param([string]$Token, [string]$Key)
|
||||||
|
if ($Token.Length -lt $Key.Length) { return $false }
|
||||||
|
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
|
||||||
|
if ($Token.Length -eq $Key.Length) { return $true }
|
||||||
|
return -not [char]::IsLetter($Token[$Key.Length])
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ProjectExtraArgs {
|
||||||
|
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
|
||||||
|
param([string]$Name)
|
||||||
|
$dir = (Get-Location).Path
|
||||||
|
while ($dir) {
|
||||||
|
$pf = Join-Path $dir ".v8-project.json"
|
||||||
|
if (Test-Path $pf) {
|
||||||
|
try {
|
||||||
|
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
|
||||||
|
} catch {}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
$parent = Split-Path $dir -Parent
|
||||||
|
if (-not $parent -or $parent -eq $dir) { break }
|
||||||
|
$dir = $parent
|
||||||
|
}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Assert-ExtraArgs {
|
||||||
|
# The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
# output key fails with an opaque 1C error — reject what the skill owns itself.
|
||||||
|
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
|
||||||
|
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
|
||||||
|
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
|
||||||
|
foreach ($tok in $ExtraArgs) {
|
||||||
|
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
|
||||||
|
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
foreach ($k in $owned) {
|
||||||
|
if (Test-ArgKeyMatch $tok $k) {
|
||||||
|
$hint = ''
|
||||||
|
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
|
||||||
|
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-ExtraArgs {
|
||||||
|
# Pick the argument list for the selected engine and validate it. An explicitly passed
|
||||||
|
# parameter for the other engine is an error; the same keys coming from .v8-project.json
|
||||||
|
# simply do not apply — a project may describe both engines.
|
||||||
|
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
|
||||||
|
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
|
||||||
|
# space-separated values spill into positional ones, a comma-joined list arrives as a
|
||||||
|
# single token. So accept the repo's list convention (comma-separated) and split here;
|
||||||
|
# a native array call keeps working. A value containing a comma is not supported.
|
||||||
|
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -eq 'ibcmd') {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
|
||||||
|
} else {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
|
||||||
|
}
|
||||||
|
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
|
||||||
|
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
|
||||||
|
# would nest the array — the tokens would then be glued into one argument.
|
||||||
|
return $extra
|
||||||
|
}
|
||||||
|
|
||||||
|
function Format-ArgsForDisplay {
|
||||||
|
# Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
# a leaked password does.
|
||||||
|
param([string[]]$ArgList, [string]$Engine)
|
||||||
|
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
|
||||||
|
$res = @()
|
||||||
|
$maskNext = $false
|
||||||
|
foreach ($tok in $ArgList) {
|
||||||
|
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
|
||||||
|
$hit = $null
|
||||||
|
foreach ($k in $keys) {
|
||||||
|
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
|
||||||
|
}
|
||||||
|
if (-not $hit) { $res += $tok; continue }
|
||||||
|
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
|
||||||
|
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
|
||||||
|
else { $res += ($hit + '***') }
|
||||||
|
}
|
||||||
|
return ,$res
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-CleanPath {
|
||||||
|
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
# inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
|
||||||
|
param([string]$Value, [string]$ParamName)
|
||||||
|
if (-not $Value) { return $Value }
|
||||||
|
$v = $Value.Trim()
|
||||||
|
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
|
||||||
|
$v = $v.Substring(1, $v.Length - 2).Trim()
|
||||||
|
}
|
||||||
|
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
|
||||||
|
if ($v.Contains('"')) {
|
||||||
|
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
return $v
|
||||||
|
}
|
||||||
|
|
||||||
|
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
|
||||||
|
$InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath'
|
||||||
|
$InputFile = ConvertTo-CleanPath $InputFile '-InputFile'
|
||||||
|
|
||||||
|
function Assert-InfoBaseExists {
|
||||||
|
# These skills work on a ready infobase. Saying so up front beats the platform's
|
||||||
|
# "Неверные или отсутствующие параметры соединения" after a launch.
|
||||||
|
param([string]$Path)
|
||||||
|
if (-not $Path) { return }
|
||||||
|
if (-not (Test-Path (Join-Path $Path "1Cv8.1CD"))) {
|
||||||
|
Write-Host "Error: information base not found at $Path (no 1Cv8.1CD)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert-InfoBaseExists $InfoBasePath
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
function Find-ProjectV8Path {
|
||||||
$dir = (Get-Location).Path
|
$dir = (Get-Location).Path
|
||||||
@@ -102,7 +297,7 @@ if (-not $V8Path) {
|
|||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,13 +306,87 @@ if (Test-Path $V8Path -PathType Container) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
|
function ConvertFrom-PlatformBytes {
|
||||||
|
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
|
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
|
||||||
|
# one of them outright mangles Cyrillic.
|
||||||
|
param([byte[]]$Bytes)
|
||||||
|
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
|
||||||
|
try {
|
||||||
|
$strict = New-Object System.Text.UTF8Encoding($false, $true)
|
||||||
|
return $strict.GetString($Bytes)
|
||||||
|
} catch {
|
||||||
|
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-PlatformProcess {
|
||||||
|
# Run the platform non-interactively and capture its console output. A closed stdin pipe
|
||||||
|
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
|
||||||
|
# text out of our stream until we print it labelled (and out of the wrong encoding).
|
||||||
|
# Returns @{ Output; ExitCode }.
|
||||||
|
#
|
||||||
|
# Quoting differs by engine, so the caller says which it built:
|
||||||
|
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
|
||||||
|
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
|
||||||
|
# which is where 1C's own parser expects them; quoting again breaks the value.
|
||||||
|
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = if ($PreQuoted) {
|
||||||
|
$ProcArgs -join ' '
|
||||||
|
} else {
|
||||||
|
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
}
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
# stderr is drained in parallel: reading the streams one after another deadlocks
|
||||||
|
# as soon as the other one fills its pipe buffer.
|
||||||
|
$errMs = New-Object System.IO.MemoryStream
|
||||||
|
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
|
||||||
|
$outMs = New-Object System.IO.MemoryStream
|
||||||
|
$p.StandardOutput.BaseStream.CopyTo($outMs)
|
||||||
|
$errTask.Wait()
|
||||||
|
$p.WaitForExit()
|
||||||
|
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
|
||||||
|
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-PlatformOutput {
|
||||||
|
# Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
|
||||||
|
param([string]$Text)
|
||||||
|
if (-not $Text) { return }
|
||||||
|
$t = $Text.TrimEnd()
|
||||||
|
if (-not $t) { return }
|
||||||
|
$limit = 65536
|
||||||
|
if ($t.Length -gt $limit) {
|
||||||
|
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
|
||||||
|
}
|
||||||
|
Write-Host "--- Вывод платформы ---"
|
||||||
|
Write-Host $t
|
||||||
|
Write-Host "--- End ---"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' }
|
||||||
|
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if ($engine -eq "ibcmd") {
|
if ($engine -eq "ibcmd") {
|
||||||
if (-not $InfoBasePath) {
|
if (-not $InfoBasePath) {
|
||||||
@@ -149,15 +418,18 @@ try {
|
|||||||
$arguments += "$InputFile"
|
$arguments += "$InputFile"
|
||||||
|
|
||||||
$arguments += "--data=$tempDir"
|
$arguments += "--data=$tempDir"
|
||||||
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
|
||||||
$output = & $V8Path @arguments 2>&1
|
$arguments += $extraArgs
|
||||||
$exitCode = $LASTEXITCODE
|
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
||||||
|
$__ib = Invoke-PlatformProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
Write-Host "Information base restored successfully from: $InputFile" -ForegroundColor Green
|
Write-Host "Information base restored successfully from: $InputFile" -ForegroundColor Green
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error restoring information base (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error restoring information base (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
if ($output) { Write-Host ($output | Out-String) }
|
Write-PlatformOutput $output
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,17 +454,18 @@ try {
|
|||||||
$outFile = Join-Path $tempDir "load_dt_log.txt"
|
$outFile = Join-Path $tempDir "load_dt_log.txt"
|
||||||
$arguments += "/Out", "`"$outFile`""
|
$arguments += "/Out", "`"$outFile`""
|
||||||
$arguments += "/DisableStartupDialogs"
|
$arguments += "/DisableStartupDialogs"
|
||||||
|
$arguments += $extraArgs
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
|
||||||
$exitCode = $process.ExitCode
|
$exitCode = $__v8.ExitCode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
Write-Host "Information base restored successfully from: $InputFile" -ForegroundColor Green
|
Write-Host "Information base restored successfully from: $InputFile" -ForegroundColor Green
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error restoring information base (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error restoring information base (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Test-Path $outFile) {
|
if (Test-Path $outFile) {
|
||||||
@@ -203,6 +476,7 @@ try {
|
|||||||
Write-Host "--- End ---"
|
Write-Host "--- End ---"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Write-PlatformOutput $__v8.Output
|
||||||
|
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-load-dt v1.3 — Load 1C information base from DT file
|
# db-load-dt v1.12 — 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
|
||||||
@@ -36,36 +36,341 @@ def _find_project_v8path():
|
|||||||
d = parent
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
V8_OWNED_KEYS = [
|
||||||
|
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
|
||||||
|
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
|
||||||
|
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
|
||||||
|
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
|
||||||
|
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||||
|
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||||
|
]
|
||||||
|
IBCMD_OWNED_KEYS = [
|
||||||
|
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||||
|
"--import", "--export", "--apply", "--force", "--create-database",
|
||||||
|
"--user", "--password",
|
||||||
|
]
|
||||||
|
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
|
||||||
|
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
|
||||||
|
|
||||||
|
|
||||||
|
def arg_key_match(token, key):
|
||||||
|
"""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
|
||||||
|
/ClearCache distinct from /C."""
|
||||||
|
if len(token) < len(key):
|
||||||
|
return False
|
||||||
|
if token[: len(key)].lower() != key.lower():
|
||||||
|
return False
|
||||||
|
if len(token) == len(key):
|
||||||
|
return True
|
||||||
|
return not token[len(key)].isalpha()
|
||||||
|
|
||||||
|
|
||||||
|
def project_extra_args(name):
|
||||||
|
"""v8args / ibcmdargs from .v8-project.json — same upward walk as v8path."""
|
||||||
|
d = os.getcwd()
|
||||||
|
while True:
|
||||||
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pf):
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
v = data.get(name)
|
||||||
|
if v:
|
||||||
|
return [str(x) for x in v]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return []
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return []
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def assert_extra_args(extra, engine, hints):
|
||||||
|
"""The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
output key fails with an opaque 1C error — reject what the skill owns itself."""
|
||||||
|
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
|
||||||
|
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
|
||||||
|
for tok in extra:
|
||||||
|
if engine == "ibcmd" and not tok.startswith("-"):
|
||||||
|
print(
|
||||||
|
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||||
|
f"({param} cannot extend the ibcmd command)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
for k in owned:
|
||||||
|
if arg_key_match(tok, k):
|
||||||
|
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||||
|
print(
|
||||||
|
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def format_args_for_display(arglist, engine):
|
||||||
|
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
a leaked password does."""
|
||||||
|
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
|
||||||
|
res = []
|
||||||
|
mask_next = False
|
||||||
|
for tok in arglist:
|
||||||
|
if mask_next:
|
||||||
|
res.append("***")
|
||||||
|
mask_next = False
|
||||||
|
continue
|
||||||
|
hit = None
|
||||||
|
for k in keys:
|
||||||
|
if tok[: len(k)].lower() == k.lower():
|
||||||
|
hit = k
|
||||||
|
break
|
||||||
|
if hit is None:
|
||||||
|
res.append(tok)
|
||||||
|
elif len(tok) == len(hit):
|
||||||
|
res.append(tok)
|
||||||
|
mask_next = True
|
||||||
|
elif tok[len(hit)] == "=":
|
||||||
|
res.append(hit + "=***")
|
||||||
|
else:
|
||||||
|
res.append(hit + "***")
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def extract_extra_args(argv, known_opts):
|
||||||
|
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
|
||||||
|
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
|
||||||
|
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
|
||||||
|
rest, v8, ibcmd = [], [], []
|
||||||
|
i = 0
|
||||||
|
while i < len(argv):
|
||||||
|
low = argv[i].lower()
|
||||||
|
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
|
||||||
|
target = v8 if low == "-additionalv8arguments" else ibcmd
|
||||||
|
i += 1
|
||||||
|
while i < len(argv) and argv[i].lower() not in known_opts:
|
||||||
|
target.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
rest.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
return rest, v8, ibcmd
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
||||||
|
"""Pick the argument list for the selected engine and validate it. An explicitly
|
||||||
|
passed parameter for the other engine is an error; the same keys coming from
|
||||||
|
.v8-project.json simply do not apply — a project may describe both engines.
|
||||||
|
|
||||||
|
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
|
||||||
|
so that form is the documented one and both ports must accept it. A value containing
|
||||||
|
a comma is not supported."""
|
||||||
|
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
|
||||||
|
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
|
||||||
|
if engine == "ibcmd" and v8_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||||
|
"(use -AdditionalIbcmdArguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine != "ibcmd" and ibcmd_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||||
|
"(use -AdditionalV8Arguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine == "ibcmd":
|
||||||
|
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
|
||||||
|
else:
|
||||||
|
extra = project_extra_args("v8args") + list(v8_extra)
|
||||||
|
if extra:
|
||||||
|
assert_extra_args(extra, engine, hints)
|
||||||
|
return extra
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
def _version_key(p):
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
"""Numeric sort key from version dir name."""
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def resolve_v8path(v8path):
|
||||||
"""Resolve path to 1cv8.exe."""
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
if not v8path:
|
if not v8path:
|
||||||
v8path = _find_project_v8path()
|
v8path = _find_project_v8path()
|
||||||
if not v8path:
|
if not v8path:
|
||||||
candidates = (
|
if os.name == "nt":
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
candidates = (
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
)
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
if candidates:
|
if candidates:
|
||||||
v8path = max(candidates, key=_version_key)
|
v8path = max(candidates, key=_version_key)
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
else:
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
if os.path.isdir(v8path):
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def decode_platform_bytes(data):
|
||||||
|
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
|
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — the locale
|
||||||
|
code page (what text=True uses) mangles both."""
|
||||||
|
if not data:
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
return data.decode("utf-8")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
return data.decode("cp866", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def assert_infobase_exists(path):
|
||||||
|
"""These skills work on a ready infobase. Saying so up front beats the platform's
|
||||||
|
"Неверные или отсутствующие параметры соединения" after a launch."""
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
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)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def clean_path(value, param=""):
|
||||||
|
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
|
||||||
|
if not value:
|
||||||
|
return value
|
||||||
|
v = value.strip()
|
||||||
|
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
|
||||||
|
v = v[1:-1].strip()
|
||||||
|
if len(v) > 3 and v[-1] in "\\/":
|
||||||
|
v = v[:-1]
|
||||||
|
if '"' in v:
|
||||||
|
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
def quote_if_needed(token):
|
||||||
|
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
|
||||||
|
verbatim, so a token with a space needs quotes of its own."""
|
||||||
|
if token and (" " in token or "\t" in token) and '"' not in token:
|
||||||
|
return f'"{token}"'
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
def run_v8(v8path, arguments):
|
||||||
|
"""Run 1cv8 in batch mode and capture its console output.
|
||||||
|
|
||||||
|
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
|
||||||
|
escape those quotes, so there the command line is handed over ready-made.
|
||||||
|
"""
|
||||||
|
if os.name == "nt":
|
||||||
|
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||||
|
else:
|
||||||
|
cmd = [v8path] + arguments
|
||||||
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def print_platform_output(result):
|
||||||
|
"""Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
|
||||||
|
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
|
||||||
|
if not text:
|
||||||
|
return
|
||||||
|
limit = 65536
|
||||||
|
if len(text) > limit:
|
||||||
|
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
|
||||||
|
print("--- Вывод платформы ---")
|
||||||
|
print(text)
|
||||||
|
print("--- End ---")
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def describe_exit(code):
|
||||||
|
"""Annotate an abnormal process exit code so a crash isn't reported as a bare number.
|
||||||
|
Batch 1C in a broken/headless environment (no GUI session, no license) can crash mid-run
|
||||||
|
instead of returning a clean error, possibly leaving the infobase locked or half-mutated."""
|
||||||
|
if code is None:
|
||||||
|
return ""
|
||||||
|
win = {
|
||||||
|
3221225477: "0xC0000005 (access violation)", -1073741819: "0xC0000005 (access violation)",
|
||||||
|
3221225781: "0xC0000135 (missing DLL)", -1073741515: "0xC0000135 (missing DLL)",
|
||||||
|
3221226505: "0xC0000409 (stack overrun)", -1073740791: "0xC0000409 (stack overrun)",
|
||||||
|
}
|
||||||
|
if code in win:
|
||||||
|
return f" — abnormal termination, exception {win[code]}; the infobase may be left in an inconsistent state; verify it before retrying"
|
||||||
|
if -64 <= code < 0:
|
||||||
|
try:
|
||||||
|
import signal
|
||||||
|
name = signal.Signals(-code).name
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
name = f"signal {-code}"
|
||||||
|
return (f" — process terminated by {name} (abnormal termination, not a normal exit); "
|
||||||
|
"the infobase may be left in an inconsistent state; verify it before retrying")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _redact(text, *secrets):
|
||||||
|
"""Redact literal secret values (password, user) from a display string —
|
||||||
|
precise, never touches lookalike paths."""
|
||||||
|
for s in secrets:
|
||||||
|
if s:
|
||||||
|
text = text.replace(s, "***")
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
@@ -82,11 +387,34 @@ def main():
|
|||||||
parser.add_argument("-InputFile", required=True)
|
parser.add_argument("-InputFile", required=True)
|
||||||
parser.add_argument("-JobsCount", type=int, default=0)
|
parser.add_argument("-JobsCount", type=int, default=0)
|
||||||
parser.add_argument("-UnlockCode", default="")
|
parser.add_argument("-UnlockCode", default="")
|
||||||
args = parser.parse_args()
|
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
|
||||||
|
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
|
||||||
|
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
|
||||||
|
help="Extra ibcmd arguments in --key=value form")
|
||||||
|
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
||||||
|
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
||||||
|
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
||||||
|
assert_infobase_exists(args.InfoBasePath)
|
||||||
|
args.InputFile = clean_path(args.InputFile, "-InputFile")
|
||||||
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
arg_hints = {
|
||||||
|
"/F": "-InfoBasePath",
|
||||||
|
"/S": "-InfoBaseServer + -InfoBaseRef",
|
||||||
|
"/N": "-UserName",
|
||||||
|
"/P": "-Password",
|
||||||
|
"--db-path": "-InfoBasePath",
|
||||||
|
"--user": "-UserName",
|
||||||
|
"--password": "-Password",
|
||||||
|
}
|
||||||
|
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if engine == "ibcmd":
|
if engine == "ibcmd":
|
||||||
if not args.InfoBasePath:
|
if not args.InfoBasePath:
|
||||||
@@ -114,16 +442,13 @@ def main():
|
|||||||
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
||||||
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
||||||
arguments.append(f"--data={ib_data}")
|
arguments.append(f"--data={ib_data}")
|
||||||
print(f"Running: ibcmd {' '.join(arguments)}")
|
arguments.extend(extra_args)
|
||||||
result = subprocess.run([v8path] + arguments, capture_output=True, encoding="utf-8", errors="replace")
|
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||||
|
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||||
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})", file=sys.stderr)
|
print(f"Error restoring information base (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
|
||||||
if result.stdout:
|
|
||||||
print(result.stdout)
|
|
||||||
if result.stderr:
|
|
||||||
print(result.stderr, file=sys.stderr)
|
|
||||||
sys.exit(result.returncode)
|
sys.exit(result.returncode)
|
||||||
|
|
||||||
# --- Temp dir ---
|
# --- Temp dir ---
|
||||||
@@ -135,40 +460,37 @@ def main():
|
|||||||
arguments = ["DESIGNER"]
|
arguments = ["DESIGNER"]
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
if args.InfoBaseServer and args.InfoBaseRef:
|
||||||
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
|
arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
|
||||||
else:
|
else:
|
||||||
arguments.extend(["/F", args.InfoBasePath])
|
arguments.extend(["/F", f'"{args.InfoBasePath}"'])
|
||||||
|
|
||||||
if args.UserName:
|
if args.UserName:
|
||||||
arguments.append(f"/N{args.UserName}")
|
arguments.append(f'/N"{args.UserName}"')
|
||||||
if args.Password:
|
if args.Password:
|
||||||
arguments.append(f"/P{args.Password}")
|
arguments.append(f'/P"{args.Password}"')
|
||||||
if args.UnlockCode:
|
if args.UnlockCode:
|
||||||
arguments.append(f"/UC{args.UnlockCode}")
|
arguments.append(f'/UC"{args.UnlockCode}"')
|
||||||
|
|
||||||
arguments.extend(["/RestoreIB", args.InputFile])
|
arguments.extend(["/RestoreIB", f'"{args.InputFile}"'])
|
||||||
if args.JobsCount > 0:
|
if args.JobsCount > 0:
|
||||||
arguments.extend(["-JobsCount", str(args.JobsCount)])
|
arguments.extend(["-JobsCount", str(args.JobsCount)])
|
||||||
|
|
||||||
# --- Output ---
|
# --- Output ---
|
||||||
out_file = os.path.join(temp_dir, "load_dt_log.txt")
|
out_file = os.path.join(temp_dir, "load_dt_log.txt")
|
||||||
arguments.extend(["/Out", out_file])
|
arguments.extend(["/Out", f'"{out_file}"'])
|
||||||
arguments.append("/DisableStartupDialogs")
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
arguments.extend(quote_if_needed(a) for a in extra_args)
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||||
result = subprocess.run(
|
result = run_v8(v8path, arguments)
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
exit_code = result.returncode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
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})", file=sys.stderr)
|
print(f"Error restoring information base (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
|
||||||
|
|
||||||
if os.path.isfile(out_file):
|
if os.path.isfile(out_file):
|
||||||
try:
|
try:
|
||||||
@@ -181,6 +503,7 @@ def main():
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
print_platform_output(result)
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" <
|
|||||||
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
|
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
|
||||||
| `-DryRun` | нет | Только показать что будет загружено (без загрузки) |
|
| `-DryRun` | нет | Только показать что будет загружено (без загрузки) |
|
||||||
| `-UpdateDB` | нет | После загрузки сразу обновить конфигурацию БД (`/UpdateDBCfg`) |
|
| `-UpdateDB` | нет | После загрузки сразу обновить конфигурацию БД (`/UpdateDBCfg`) |
|
||||||
|
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
|
||||||
|
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
|
||||||
|
|
||||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# db-load-git v1.8 — Load Git changes into 1C database
|
# db-load-git v1.18 — Load Git changes into 1C database
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Загрузка изменений из Git в базу 1С
|
Загрузка изменений из Git в базу 1С
|
||||||
@@ -47,6 +48,12 @@
|
|||||||
.PARAMETER DryRun
|
.PARAMETER DryRun
|
||||||
Только показать что будет загружено (без загрузки)
|
Только показать что будет загружено (без загрузки)
|
||||||
|
|
||||||
|
.PARAMETER AdditionalV8Arguments
|
||||||
|
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
|
||||||
|
|
||||||
|
.PARAMETER AdditionalIbcmdArguments
|
||||||
|
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
|
||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
.\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Source All
|
.\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Source All
|
||||||
|
|
||||||
@@ -101,12 +108,164 @@ param(
|
|||||||
[switch]$DryRun,
|
[switch]$DryRun,
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[switch]$UpdateDB
|
[switch]$UpdateDB,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalV8Arguments = @(),
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalIbcmdArguments = @()
|
||||||
)
|
)
|
||||||
|
|
||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
function Protect-Secrets {
|
||||||
|
# Redact literal secret values from a display string (String.Replace is literal, not regex).
|
||||||
|
param([string]$Text, [string[]]$Secrets)
|
||||||
|
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
|
||||||
|
return $Text
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
$script:V8OwnedKeys = @(
|
||||||
|
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
|
||||||
|
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
|
||||||
|
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
|
||||||
|
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
|
||||||
|
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||||
|
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||||
|
)
|
||||||
|
$script:IbcmdOwnedKeys = @(
|
||||||
|
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||||
|
'--import', '--export', '--apply', '--force', '--create-database',
|
||||||
|
'--user', '--password'
|
||||||
|
)
|
||||||
|
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
|
||||||
|
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
|
||||||
|
|
||||||
|
function Test-ArgKeyMatch {
|
||||||
|
# A token matches a key when it equals the key, or starts with it and the next
|
||||||
|
# character is not a letter — catches glued /N"user" and --password=x, while
|
||||||
|
# keeping /ClearCache distinct from /C.
|
||||||
|
param([string]$Token, [string]$Key)
|
||||||
|
if ($Token.Length -lt $Key.Length) { return $false }
|
||||||
|
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
|
||||||
|
if ($Token.Length -eq $Key.Length) { return $true }
|
||||||
|
return -not [char]::IsLetter($Token[$Key.Length])
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ProjectExtraArgs {
|
||||||
|
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
|
||||||
|
param([string]$Name)
|
||||||
|
$dir = (Get-Location).Path
|
||||||
|
while ($dir) {
|
||||||
|
$pf = Join-Path $dir ".v8-project.json"
|
||||||
|
if (Test-Path $pf) {
|
||||||
|
try {
|
||||||
|
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
|
||||||
|
} catch {}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
$parent = Split-Path $dir -Parent
|
||||||
|
if (-not $parent -or $parent -eq $dir) { break }
|
||||||
|
$dir = $parent
|
||||||
|
}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Assert-ExtraArgs {
|
||||||
|
# The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
# output key fails with an opaque 1C error — reject what the skill owns itself.
|
||||||
|
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
|
||||||
|
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
|
||||||
|
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
|
||||||
|
foreach ($tok in $ExtraArgs) {
|
||||||
|
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
|
||||||
|
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
foreach ($k in $owned) {
|
||||||
|
if (Test-ArgKeyMatch $tok $k) {
|
||||||
|
$hint = ''
|
||||||
|
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
|
||||||
|
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-ExtraArgs {
|
||||||
|
# Pick the argument list for the selected engine and validate it. An explicitly passed
|
||||||
|
# parameter for the other engine is an error; the same keys coming from .v8-project.json
|
||||||
|
# simply do not apply — a project may describe both engines.
|
||||||
|
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
|
||||||
|
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
|
||||||
|
# space-separated values spill into positional ones, a comma-joined list arrives as a
|
||||||
|
# single token. So accept the repo's list convention (comma-separated) and split here;
|
||||||
|
# a native array call keeps working. A value containing a comma is not supported.
|
||||||
|
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -eq 'ibcmd') {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
|
||||||
|
} else {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
|
||||||
|
}
|
||||||
|
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
|
||||||
|
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
|
||||||
|
# would nest the array — the tokens would then be glued into one argument.
|
||||||
|
return $extra
|
||||||
|
}
|
||||||
|
|
||||||
|
function Format-ArgsForDisplay {
|
||||||
|
# Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
# a leaked password does.
|
||||||
|
param([string[]]$ArgList, [string]$Engine)
|
||||||
|
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
|
||||||
|
$res = @()
|
||||||
|
$maskNext = $false
|
||||||
|
foreach ($tok in $ArgList) {
|
||||||
|
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
|
||||||
|
$hit = $null
|
||||||
|
foreach ($k in $keys) {
|
||||||
|
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
|
||||||
|
}
|
||||||
|
if (-not $hit) { $res += $tok; continue }
|
||||||
|
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
|
||||||
|
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
|
||||||
|
else { $res += ($hit + '***') }
|
||||||
|
}
|
||||||
|
return ,$res
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ExitAnnotation {
|
||||||
|
# Annotate an abnormal process exit code so a crash isn't reported as a bare number.
|
||||||
|
# A batch DESIGNER that crashes (e.g. missing license) may leave the infobase locked or
|
||||||
|
# half-updated — surface that instead of a plain code. (Windows exception codes only;
|
||||||
|
# POSIX signals are handled in the .py port.)
|
||||||
|
param([int]$Code)
|
||||||
|
$win = @{
|
||||||
|
-1073741819 = "0xC0000005 (access violation)"
|
||||||
|
-1073741515 = "0xC0000135 (missing DLL)"
|
||||||
|
-1073740791 = "0xC0000409 (stack overrun)"
|
||||||
|
}
|
||||||
|
if ($win.ContainsKey($Code)) {
|
||||||
|
return " — abnormal termination, exception $($win[$Code]); the infobase may be left in an inconsistent state; verify it before retrying"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
# --- Helper: map sub-file path (BSL, HTML, etc.) to object XML ---
|
# --- Helper: map sub-file path (BSL, HTML, etc.) to object XML ---
|
||||||
function Get-ObjectXmlFromSubFile {
|
function Get-ObjectXmlFromSubFile {
|
||||||
param([string]$RelativePath)
|
param([string]$RelativePath)
|
||||||
@@ -149,7 +308,7 @@ if (-not $DryRun) {
|
|||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -158,7 +317,7 @@ if (-not $DryRun) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -166,6 +325,76 @@ if (-not $DryRun) {
|
|||||||
# --- Detect engine + validate connection (skip if DryRun) ---
|
# --- Detect engine + validate connection (skip if DryRun) ---
|
||||||
$engine = "1cv8"
|
$engine = "1cv8"
|
||||||
if (-not $DryRun) {
|
if (-not $DryRun) {
|
||||||
|
function ConvertFrom-PlatformBytes {
|
||||||
|
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
|
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
|
||||||
|
# one of them outright mangles Cyrillic.
|
||||||
|
param([byte[]]$Bytes)
|
||||||
|
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
|
||||||
|
try {
|
||||||
|
$strict = New-Object System.Text.UTF8Encoding($false, $true)
|
||||||
|
return $strict.GetString($Bytes)
|
||||||
|
} catch {
|
||||||
|
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-PlatformProcess {
|
||||||
|
# Run the platform non-interactively and capture its console output. A closed stdin pipe
|
||||||
|
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
|
||||||
|
# text out of our stream until we print it labelled (and out of the wrong encoding).
|
||||||
|
# Returns @{ Output; ExitCode }.
|
||||||
|
#
|
||||||
|
# Quoting differs by engine, so the caller says which it built:
|
||||||
|
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
|
||||||
|
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
|
||||||
|
# which is where 1C's own parser expects them; quoting again breaks the value.
|
||||||
|
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = if ($PreQuoted) {
|
||||||
|
$ProcArgs -join ' '
|
||||||
|
} else {
|
||||||
|
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
}
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
# stderr is drained in parallel: reading the streams one after another deadlocks
|
||||||
|
# as soon as the other one fills its pipe buffer.
|
||||||
|
$errMs = New-Object System.IO.MemoryStream
|
||||||
|
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
|
||||||
|
$outMs = New-Object System.IO.MemoryStream
|
||||||
|
$p.StandardOutput.BaseStream.CopyTo($outMs)
|
||||||
|
$errTask.Wait()
|
||||||
|
$p.WaitForExit()
|
||||||
|
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
|
||||||
|
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-PlatformOutput {
|
||||||
|
# Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
|
||||||
|
param([string]$Text)
|
||||||
|
if (-not $Text) { return }
|
||||||
|
$t = $Text.TrimEnd()
|
||||||
|
if (-not $t) { return }
|
||||||
|
$limit = 65536
|
||||||
|
if ($t.Length -gt $limit) {
|
||||||
|
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
|
||||||
|
}
|
||||||
|
Write-Host "--- Вывод платформы ---"
|
||||||
|
Write-Host $t
|
||||||
|
Write-Host "--- End ---"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
if ($engine -eq "ibcmd") {
|
if ($engine -eq "ibcmd") {
|
||||||
if (-not $InfoBasePath) {
|
if (-not $InfoBasePath) {
|
||||||
@@ -178,6 +407,10 @@ if (-not $DryRun) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' }
|
||||||
|
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
|
||||||
|
|
||||||
# --- Validate config dir ---
|
# --- Validate config dir ---
|
||||||
if (-not (Test-Path $ConfigDir)) {
|
if (-not (Test-Path $ConfigDir)) {
|
||||||
Write-Host "Error: config directory not found: $ConfigDir" -ForegroundColor Red
|
Write-Host "Error: config directory not found: $ConfigDir" -ForegroundColor Red
|
||||||
@@ -199,6 +432,15 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- Get changed files from Git ---
|
# --- Get changed files from Git ---
|
||||||
|
# Все git-вызовы для сбора путей идут через один хелпер с -c core.quotePath=false,
|
||||||
|
# иначе кириллические пути возвращаются в octal-виде и не распознаются (зеркало run_git в .py).
|
||||||
|
function Invoke-GitLines {
|
||||||
|
param([string[]]$GitArgs)
|
||||||
|
$out = git -c core.quotePath=false @GitArgs 2>&1
|
||||||
|
if ($LASTEXITCODE -eq 0) { return $out }
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
$changedFiles = @()
|
$changedFiles = @()
|
||||||
$ConfigDir = (Resolve-Path $ConfigDir).Path.TrimEnd('\')
|
$ConfigDir = (Resolve-Path $ConfigDir).Path.TrimEnd('\')
|
||||||
$configDirNormalized = $ConfigDir.Replace('\', '/')
|
$configDirNormalized = $ConfigDir.Replace('\', '/')
|
||||||
@@ -208,29 +450,22 @@ try {
|
|||||||
switch ($Source) {
|
switch ($Source) {
|
||||||
"Staged" {
|
"Staged" {
|
||||||
Write-Host "Getting staged changes..."
|
Write-Host "Getting staged changes..."
|
||||||
$raw = git diff --cached --name-only --relative 2>&1
|
$changedFiles += Invoke-GitLines -GitArgs @('diff', '--cached', '--name-only', '--relative')
|
||||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
|
||||||
}
|
}
|
||||||
"Unstaged" {
|
"Unstaged" {
|
||||||
Write-Host "Getting unstaged changes..."
|
Write-Host "Getting unstaged changes..."
|
||||||
$raw = git diff --name-only --relative 2>&1
|
$changedFiles += Invoke-GitLines -GitArgs @('diff', '--name-only', '--relative')
|
||||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
$changedFiles += Invoke-GitLines -GitArgs @('ls-files', '--others', '--exclude-standard')
|
||||||
$raw = git ls-files --others --exclude-standard 2>&1
|
|
||||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
|
||||||
}
|
}
|
||||||
"Commit" {
|
"Commit" {
|
||||||
Write-Host "Getting changes from $CommitRange..."
|
Write-Host "Getting changes from $CommitRange..."
|
||||||
$raw = git diff --name-only --relative $CommitRange 2>&1
|
$changedFiles += Invoke-GitLines -GitArgs @('diff', '--name-only', '--relative', $CommitRange)
|
||||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
|
||||||
}
|
}
|
||||||
"All" {
|
"All" {
|
||||||
Write-Host "Getting all uncommitted changes..."
|
Write-Host "Getting all uncommitted changes..."
|
||||||
$raw = git diff --cached --name-only --relative 2>&1
|
$changedFiles += Invoke-GitLines -GitArgs @('diff', '--cached', '--name-only', '--relative')
|
||||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
$changedFiles += Invoke-GitLines -GitArgs @('diff', '--name-only', '--relative')
|
||||||
$raw = git diff --name-only --relative 2>&1
|
$changedFiles += Invoke-GitLines -GitArgs @('ls-files', '--others', '--exclude-standard')
|
||||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
|
||||||
$raw = git ls-files --others --exclude-standard 2>&1
|
|
||||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -342,30 +577,34 @@ try {
|
|||||||
if ($UserName) { $arguments += "--user=$UserName" }
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
if ($Password) { $arguments += "--password=$Password" }
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
$arguments += "--data=$tempDir"
|
$arguments += "--data=$tempDir"
|
||||||
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
$arguments += $extraArgs
|
||||||
$output = & $V8Path @arguments 2>&1
|
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
||||||
$exitCode = $LASTEXITCODE
|
$__ib = Invoke-PlatformProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
if ($exitCode -ne 0) {
|
if ($exitCode -ne 0) {
|
||||||
Write-Host "Error loading changes (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error loading changes (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||||
if ($output) { Write-Host ($output | Out-String) }
|
Write-PlatformOutput $output
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
}
|
}
|
||||||
Write-Host "Changes loaded successfully ($($configFiles.Count) files)" -ForegroundColor Green
|
Write-Host "Changes loaded successfully ($($configFiles.Count) files)" -ForegroundColor Green
|
||||||
if ($output) { Write-Host ($output | Out-String) }
|
Write-PlatformOutput $output
|
||||||
if ($UpdateDB) {
|
if ($UpdateDB) {
|
||||||
$applyArgs = @("infobase", "config", "apply", "--db-path=$InfoBasePath", "--force")
|
$applyArgs = @("infobase", "config", "apply", "--db-path=$InfoBasePath", "--force")
|
||||||
if ($UserName) { $applyArgs += "--user=$UserName" }
|
if ($UserName) { $applyArgs += "--user=$UserName" }
|
||||||
if ($Password) { $applyArgs += "--password=$Password" }
|
if ($Password) { $applyArgs += "--password=$Password" }
|
||||||
$applyArgs += "--data=$tempDir"
|
$applyArgs += "--data=$tempDir"
|
||||||
Write-Host "Running: ibcmd $($applyArgs -join ' ')"
|
$applyArgs += $extraArgs
|
||||||
$applyOut = & $V8Path @applyArgs 2>&1
|
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $applyArgs $engine) -join ' ') @($Password, $UserName))"
|
||||||
$exitCode = $LASTEXITCODE
|
$__ib = Invoke-PlatformProcess $V8Path $applyArgs
|
||||||
|
$applyOut = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
Write-Host "Database configuration updated successfully" -ForegroundColor Green
|
Write-Host "Database configuration updated successfully" -ForegroundColor Green
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error updating database configuration (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
if ($applyOut) { Write-Host ($applyOut | Out-String) }
|
Write-PlatformOutput $applyOut
|
||||||
}
|
}
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
}
|
}
|
||||||
@@ -410,21 +649,22 @@ try {
|
|||||||
$outFile = Join-Path $tempDir "load_log.txt"
|
$outFile = Join-Path $tempDir "load_log.txt"
|
||||||
$arguments += "/Out", "`"$outFile`""
|
$arguments += "/Out", "`"$outFile`""
|
||||||
$arguments += "/DisableStartupDialogs"
|
$arguments += "/DisableStartupDialogs"
|
||||||
|
$arguments += $extraArgs
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "Executing partial configuration load..."
|
Write-Host "Executing partial configuration load..."
|
||||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
||||||
|
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
|
||||||
$exitCode = $process.ExitCode
|
$exitCode = $__v8.ExitCode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
Write-Host "Load completed successfully" -ForegroundColor Green
|
Write-Host "Load completed successfully" -ForegroundColor Green
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error loading configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Test-Path $outFile) {
|
if (Test-Path $outFile) {
|
||||||
@@ -435,6 +675,7 @@ try {
|
|||||||
Write-Host "--- End ---"
|
Write-Host "--- End ---"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Write-PlatformOutput $__v8.Output
|
||||||
|
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-load-git v1.8 — Load Git changes into 1C database
|
# db-load-git v1.18 — 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
|
||||||
@@ -36,36 +36,308 @@ def _find_project_v8path():
|
|||||||
d = parent
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
V8_OWNED_KEYS = [
|
||||||
|
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
|
||||||
|
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
|
||||||
|
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
|
||||||
|
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
|
||||||
|
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||||
|
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||||
|
]
|
||||||
|
IBCMD_OWNED_KEYS = [
|
||||||
|
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||||
|
"--import", "--export", "--apply", "--force", "--create-database",
|
||||||
|
"--user", "--password",
|
||||||
|
]
|
||||||
|
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
|
||||||
|
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
|
||||||
|
|
||||||
|
|
||||||
|
def arg_key_match(token, key):
|
||||||
|
"""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
|
||||||
|
/ClearCache distinct from /C."""
|
||||||
|
if len(token) < len(key):
|
||||||
|
return False
|
||||||
|
if token[: len(key)].lower() != key.lower():
|
||||||
|
return False
|
||||||
|
if len(token) == len(key):
|
||||||
|
return True
|
||||||
|
return not token[len(key)].isalpha()
|
||||||
|
|
||||||
|
|
||||||
|
def project_extra_args(name):
|
||||||
|
"""v8args / ibcmdargs from .v8-project.json — same upward walk as v8path."""
|
||||||
|
d = os.getcwd()
|
||||||
|
while True:
|
||||||
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pf):
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
v = data.get(name)
|
||||||
|
if v:
|
||||||
|
return [str(x) for x in v]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return []
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return []
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def assert_extra_args(extra, engine, hints):
|
||||||
|
"""The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
output key fails with an opaque 1C error — reject what the skill owns itself."""
|
||||||
|
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
|
||||||
|
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
|
||||||
|
for tok in extra:
|
||||||
|
if engine == "ibcmd" and not tok.startswith("-"):
|
||||||
|
print(
|
||||||
|
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||||
|
f"({param} cannot extend the ibcmd command)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
for k in owned:
|
||||||
|
if arg_key_match(tok, k):
|
||||||
|
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||||
|
print(
|
||||||
|
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def format_args_for_display(arglist, engine):
|
||||||
|
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
a leaked password does."""
|
||||||
|
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
|
||||||
|
res = []
|
||||||
|
mask_next = False
|
||||||
|
for tok in arglist:
|
||||||
|
if mask_next:
|
||||||
|
res.append("***")
|
||||||
|
mask_next = False
|
||||||
|
continue
|
||||||
|
hit = None
|
||||||
|
for k in keys:
|
||||||
|
if tok[: len(k)].lower() == k.lower():
|
||||||
|
hit = k
|
||||||
|
break
|
||||||
|
if hit is None:
|
||||||
|
res.append(tok)
|
||||||
|
elif len(tok) == len(hit):
|
||||||
|
res.append(tok)
|
||||||
|
mask_next = True
|
||||||
|
elif tok[len(hit)] == "=":
|
||||||
|
res.append(hit + "=***")
|
||||||
|
else:
|
||||||
|
res.append(hit + "***")
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def extract_extra_args(argv, known_opts):
|
||||||
|
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
|
||||||
|
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
|
||||||
|
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
|
||||||
|
rest, v8, ibcmd = [], [], []
|
||||||
|
i = 0
|
||||||
|
while i < len(argv):
|
||||||
|
low = argv[i].lower()
|
||||||
|
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
|
||||||
|
target = v8 if low == "-additionalv8arguments" else ibcmd
|
||||||
|
i += 1
|
||||||
|
while i < len(argv) and argv[i].lower() not in known_opts:
|
||||||
|
target.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
rest.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
return rest, v8, ibcmd
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
||||||
|
"""Pick the argument list for the selected engine and validate it. An explicitly
|
||||||
|
passed parameter for the other engine is an error; the same keys coming from
|
||||||
|
.v8-project.json simply do not apply — a project may describe both engines.
|
||||||
|
|
||||||
|
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
|
||||||
|
so that form is the documented one and both ports must accept it. A value containing
|
||||||
|
a comma is not supported."""
|
||||||
|
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
|
||||||
|
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
|
||||||
|
if engine == "ibcmd" and v8_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||||
|
"(use -AdditionalIbcmdArguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine != "ibcmd" and ibcmd_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||||
|
"(use -AdditionalV8Arguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine == "ibcmd":
|
||||||
|
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
|
||||||
|
else:
|
||||||
|
extra = project_extra_args("v8args") + list(v8_extra)
|
||||||
|
if extra:
|
||||||
|
assert_extra_args(extra, engine, hints)
|
||||||
|
return extra
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
def _version_key(p):
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
"""Numeric sort key from version dir name."""
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def resolve_v8path(v8path):
|
||||||
"""Resolve path to 1cv8.exe."""
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
if not v8path:
|
if not v8path:
|
||||||
v8path = _find_project_v8path()
|
v8path = _find_project_v8path()
|
||||||
if not v8path:
|
if not v8path:
|
||||||
candidates = (
|
if os.name == "nt":
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
candidates = (
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
)
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
if candidates:
|
if candidates:
|
||||||
v8path = max(candidates, key=_version_key)
|
v8path = max(candidates, key=_version_key)
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
else:
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
if os.path.isdir(v8path):
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def decode_platform_bytes(data):
|
||||||
|
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
|
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — the locale
|
||||||
|
code page (what text=True uses) mangles both."""
|
||||||
|
if not data:
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
return data.decode("utf-8")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
return data.decode("cp866", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def assert_infobase_exists(path):
|
||||||
|
"""These skills work on a ready infobase. Saying so up front beats the platform's
|
||||||
|
"Неверные или отсутствующие параметры соединения" after a launch."""
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
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)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def clean_path(value, param=""):
|
||||||
|
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
|
||||||
|
if not value:
|
||||||
|
return value
|
||||||
|
v = value.strip()
|
||||||
|
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
|
||||||
|
v = v[1:-1].strip()
|
||||||
|
if len(v) > 3 and v[-1] in "\\/":
|
||||||
|
v = v[:-1]
|
||||||
|
if '"' in v:
|
||||||
|
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
def quote_if_needed(token):
|
||||||
|
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
|
||||||
|
verbatim, so a token with a space needs quotes of its own."""
|
||||||
|
if token and (" " in token or "\t" in token) and '"' not in token:
|
||||||
|
return f'"{token}"'
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
def run_v8(v8path, arguments):
|
||||||
|
"""Run 1cv8 in batch mode and capture its console output.
|
||||||
|
|
||||||
|
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
|
||||||
|
escape those quotes, so there the command line is handed over ready-made.
|
||||||
|
"""
|
||||||
|
if os.name == "nt":
|
||||||
|
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||||
|
else:
|
||||||
|
cmd = [v8path] + arguments
|
||||||
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def print_platform_output(result):
|
||||||
|
"""Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
|
||||||
|
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
|
||||||
|
if not text:
|
||||||
|
return
|
||||||
|
limit = 65536
|
||||||
|
if len(text) > limit:
|
||||||
|
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
|
||||||
|
print("--- Вывод платформы ---")
|
||||||
|
print(text)
|
||||||
|
print("--- End ---")
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
def get_object_xml_from_subfile(relative_path):
|
def get_object_xml_from_subfile(relative_path):
|
||||||
"""Map sub-file path (BSL, HTML, etc.) to object XML path."""
|
"""Map sub-file path (BSL, HTML, etc.) to object XML path."""
|
||||||
parts = re.split(r"[\\/]", relative_path)
|
parts = re.split(r"[\\/]", relative_path)
|
||||||
@@ -77,7 +349,7 @@ def get_object_xml_from_subfile(relative_path):
|
|||||||
def run_git(config_dir, git_args):
|
def run_git(config_dir, git_args):
|
||||||
"""Run a git command in config_dir and return output lines on success."""
|
"""Run a git command in config_dir and return output lines on success."""
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["git"] + git_args,
|
["git", "-c", "core.quotePath=false"] + git_args,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
@@ -88,6 +360,39 @@ def run_git(config_dir, git_args):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def describe_exit(code):
|
||||||
|
"""Annotate an abnormal process exit code so a crash isn't reported as a bare number.
|
||||||
|
Batch 1C in a broken/headless environment (no GUI session, no license) can crash mid-run
|
||||||
|
instead of returning a clean error, possibly leaving the infobase locked or half-mutated."""
|
||||||
|
if code is None:
|
||||||
|
return ""
|
||||||
|
win = {
|
||||||
|
3221225477: "0xC0000005 (access violation)", -1073741819: "0xC0000005 (access violation)",
|
||||||
|
3221225781: "0xC0000135 (missing DLL)", -1073741515: "0xC0000135 (missing DLL)",
|
||||||
|
3221226505: "0xC0000409 (stack overrun)", -1073740791: "0xC0000409 (stack overrun)",
|
||||||
|
}
|
||||||
|
if code in win:
|
||||||
|
return f" — abnormal termination, exception {win[code]}; the infobase may be left in an inconsistent state; verify it before retrying"
|
||||||
|
if -64 <= code < 0:
|
||||||
|
try:
|
||||||
|
import signal
|
||||||
|
name = signal.Signals(-code).name
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
name = f"signal {-code}"
|
||||||
|
return (f" — process terminated by {name} (abnormal termination, not a normal exit); "
|
||||||
|
"the infobase may be left in an inconsistent state; verify it before retrying")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _redact(text, *secrets):
|
||||||
|
"""Redact literal secret values (password, user) from a display string —
|
||||||
|
precise, never touches lookalike paths."""
|
||||||
|
for s in secrets:
|
||||||
|
if s:
|
||||||
|
text = text.replace(s, "***")
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
@@ -119,7 +424,18 @@ def main():
|
|||||||
)
|
)
|
||||||
parser.add_argument("-DryRun", action="store_true", help="Only show what would be loaded (no actual load)")
|
parser.add_argument("-DryRun", action="store_true", help="Only show what would be loaded (no actual load)")
|
||||||
parser.add_argument("-UpdateDB", action="store_true", help="Also update database configuration after load")
|
parser.add_argument("-UpdateDB", action="store_true", help="Also update database configuration after load")
|
||||||
args = parser.parse_args()
|
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
|
||||||
|
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
|
||||||
|
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
|
||||||
|
help="Extra ibcmd arguments in --key=value form")
|
||||||
|
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
||||||
|
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
||||||
|
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
||||||
|
assert_infobase_exists(args.InfoBasePath)
|
||||||
|
args.ConfigDir = clean_path(args.ConfigDir, "-ConfigDir")
|
||||||
|
|
||||||
# --- Resolve V8Path (skip if DryRun) ---
|
# --- Resolve V8Path (skip if DryRun) ---
|
||||||
v8path = None
|
v8path = None
|
||||||
@@ -138,6 +454,18 @@ def main():
|
|||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
arg_hints = {
|
||||||
|
"/F": "-InfoBasePath",
|
||||||
|
"/S": "-InfoBaseServer + -InfoBaseRef",
|
||||||
|
"/N": "-UserName",
|
||||||
|
"/P": "-Password",
|
||||||
|
"--db-path": "-InfoBasePath",
|
||||||
|
"--user": "-UserName",
|
||||||
|
"--password": "-Password",
|
||||||
|
}
|
||||||
|
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
|
||||||
|
|
||||||
# --- 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}", file=sys.stderr)
|
||||||
@@ -274,18 +602,13 @@ def main():
|
|||||||
if args.Password:
|
if args.Password:
|
||||||
arguments.append(f"--password={args.Password}")
|
arguments.append(f"--password={args.Password}")
|
||||||
arguments.append(f"--data={ib_data}")
|
arguments.append(f"--data={ib_data}")
|
||||||
print(f"Running: ibcmd {' '.join(arguments)}")
|
arguments.extend(extra_args)
|
||||||
result = subprocess.run([v8path] + arguments, capture_output=True, encoding="utf-8", errors="replace")
|
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, 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})", file=sys.stderr)
|
print(f"Error loading changes (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
|
||||||
if result.stdout:
|
|
||||||
print(result.stdout)
|
|
||||||
if result.stderr:
|
|
||||||
print(result.stderr, file=sys.stderr)
|
|
||||||
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)")
|
||||||
if result.stdout:
|
|
||||||
print(result.stdout)
|
|
||||||
exit_code = 0
|
exit_code = 0
|
||||||
if args.UpdateDB:
|
if args.UpdateDB:
|
||||||
apply_args = ["infobase", "config", "apply", f"--db-path={args.InfoBasePath}", "--force"]
|
apply_args = ["infobase", "config", "apply", f"--db-path={args.InfoBasePath}", "--force"]
|
||||||
@@ -294,17 +617,15 @@ def main():
|
|||||||
if args.Password:
|
if args.Password:
|
||||||
apply_args.append(f"--password={args.Password}")
|
apply_args.append(f"--password={args.Password}")
|
||||||
apply_args.append(f"--data={ib_data}")
|
apply_args.append(f"--data={ib_data}")
|
||||||
print(f"Running: ibcmd {' '.join(apply_args)}")
|
apply_args.extend(extra_args)
|
||||||
ar = subprocess.run([v8path] + apply_args, capture_output=True, encoding="utf-8", errors="replace")
|
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(apply_args, engine)), args.Password, args.UserName)}")
|
||||||
|
ar = run_ibcmd([v8path] + apply_args, bool(args.UserName))
|
||||||
exit_code = ar.returncode
|
exit_code = ar.returncode
|
||||||
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})", file=sys.stderr)
|
print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
|
||||||
if ar.stdout:
|
print_platform_output(ar)
|
||||||
print(ar.stdout)
|
|
||||||
if ar.stderr:
|
|
||||||
print(ar.stderr, file=sys.stderr)
|
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
# --- Write list file (UTF-8 with BOM) ---
|
# --- Write list file (UTF-8 with BOM) ---
|
||||||
@@ -316,24 +637,24 @@ def main():
|
|||||||
arguments = ["DESIGNER"]
|
arguments = ["DESIGNER"]
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
if args.InfoBaseServer and args.InfoBaseRef:
|
||||||
arguments += ["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"]
|
arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
|
||||||
else:
|
else:
|
||||||
arguments += ["/F", args.InfoBasePath]
|
arguments += ["/F", f'"{args.InfoBasePath}"']
|
||||||
|
|
||||||
if args.UserName:
|
if args.UserName:
|
||||||
arguments.append(f"/N{args.UserName}")
|
arguments.append(f'/N"{args.UserName}"')
|
||||||
if args.Password:
|
if args.Password:
|
||||||
arguments.append(f"/P{args.Password}")
|
arguments.append(f'/P"{args.Password}"')
|
||||||
|
|
||||||
arguments += ["/LoadConfigFromFiles", args.ConfigDir]
|
arguments += ["/LoadConfigFromFiles", f'"{args.ConfigDir}"']
|
||||||
arguments += ["-listFile", list_file]
|
arguments += ["-listFile", f'"{list_file}"']
|
||||||
arguments += ["-Format", args.Format]
|
arguments += ["-Format", args.Format]
|
||||||
arguments.append("-partial")
|
arguments.append("-partial")
|
||||||
arguments.append("-updateConfigDumpInfo")
|
arguments.append("-updateConfigDumpInfo")
|
||||||
|
|
||||||
# --- Extensions ---
|
# --- Extensions ---
|
||||||
if args.Extension:
|
if args.Extension:
|
||||||
arguments += ["-Extension", args.Extension]
|
arguments += ["-Extension", f'"{args.Extension}"']
|
||||||
elif args.AllExtensions:
|
elif args.AllExtensions:
|
||||||
arguments.append("-AllExtensions")
|
arguments.append("-AllExtensions")
|
||||||
|
|
||||||
@@ -343,19 +664,16 @@ def main():
|
|||||||
|
|
||||||
# --- Output ---
|
# --- Output ---
|
||||||
out_file = os.path.join(temp_dir, "load_log.txt")
|
out_file = os.path.join(temp_dir, "load_log.txt")
|
||||||
arguments += ["/Out", out_file]
|
arguments += ["/Out", f'"{out_file}"']
|
||||||
arguments.append("/DisableStartupDialogs")
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
arguments.extend(quote_if_needed(a) for a in extra_args)
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
print("")
|
print("")
|
||||||
print("Executing partial configuration load...")
|
print("Executing partial configuration load...")
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||||
|
|
||||||
result = subprocess.run(
|
result = run_v8(v8path, arguments)
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
exit_code = result.returncode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
@@ -363,7 +681,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})", file=sys.stderr)
|
print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
|
||||||
|
|
||||||
if os.path.isfile(out_file):
|
if os.path.isfile(out_file):
|
||||||
try:
|
try:
|
||||||
@@ -376,6 +694,7 @@ def main():
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
print_platform_output(result)
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" <
|
|||||||
| `-AllExtensions` | нет | Загрузить все расширения |
|
| `-AllExtensions` | нет | Загрузить все расширения |
|
||||||
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
|
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
|
||||||
| `-UpdateDB` | нет | После загрузки сразу обновить конфигурацию БД (`/UpdateDBCfg`) |
|
| `-UpdateDB` | нет | После загрузки сразу обновить конфигурацию БД (`/UpdateDBCfg`) |
|
||||||
|
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
|
||||||
|
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
|
||||||
|
|
||||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# db-load-xml v1.10 — Load 1C configuration from XML files
|
# db-load-xml v1.19 — 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 не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Загрузка конфигурации 1С из XML-файлов
|
Загрузка конфигурации 1С из XML-файлов
|
||||||
@@ -47,6 +48,12 @@
|
|||||||
.PARAMETER Format
|
.PARAMETER Format
|
||||||
Формат файлов: Hierarchical или Plain (по умолчанию Hierarchical)
|
Формат файлов: Hierarchical или Plain (по умолчанию Hierarchical)
|
||||||
|
|
||||||
|
.PARAMETER AdditionalV8Arguments
|
||||||
|
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
|
||||||
|
|
||||||
|
.PARAMETER AdditionalIbcmdArguments
|
||||||
|
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
|
||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
.\db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Full
|
.\db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Full
|
||||||
|
|
||||||
@@ -101,12 +108,201 @@ param(
|
|||||||
[switch]$UpdateDB,
|
[switch]$UpdateDB,
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[switch]$StrictLog
|
[switch]$StrictLog,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalV8Arguments = @(),
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalIbcmdArguments = @()
|
||||||
)
|
)
|
||||||
|
|
||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
function Protect-Secrets {
|
||||||
|
# Redact literal secret values from a display string (String.Replace is literal, not regex).
|
||||||
|
param([string]$Text, [string[]]$Secrets)
|
||||||
|
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
|
||||||
|
return $Text
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ExitAnnotation {
|
||||||
|
# Annotate an abnormal process exit code so a crash isn't reported as a bare number.
|
||||||
|
# A batch DESIGNER that crashes (e.g. missing license) may leave the infobase locked or
|
||||||
|
# half-updated — surface that instead of a plain code. (Windows exception codes only;
|
||||||
|
# POSIX signals are handled in the .py port.)
|
||||||
|
param([int]$Code)
|
||||||
|
$win = @{
|
||||||
|
-1073741819 = "0xC0000005 (access violation)"
|
||||||
|
-1073741515 = "0xC0000135 (missing DLL)"
|
||||||
|
-1073740791 = "0xC0000409 (stack overrun)"
|
||||||
|
}
|
||||||
|
if ($win.ContainsKey($Code)) {
|
||||||
|
return " — abnormal termination, exception $($win[$Code]); the infobase may be left in an inconsistent state; verify it before retrying"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
$script:V8OwnedKeys = @(
|
||||||
|
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
|
||||||
|
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
|
||||||
|
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
|
||||||
|
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
|
||||||
|
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||||
|
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||||
|
)
|
||||||
|
$script:IbcmdOwnedKeys = @(
|
||||||
|
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||||
|
'--import', '--export', '--apply', '--force', '--create-database',
|
||||||
|
'--user', '--password'
|
||||||
|
)
|
||||||
|
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
|
||||||
|
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
|
||||||
|
|
||||||
|
function Test-ArgKeyMatch {
|
||||||
|
# A token matches a key when it equals the key, or starts with it and the next
|
||||||
|
# character is not a letter — catches glued /N"user" and --password=x, while
|
||||||
|
# keeping /ClearCache distinct from /C.
|
||||||
|
param([string]$Token, [string]$Key)
|
||||||
|
if ($Token.Length -lt $Key.Length) { return $false }
|
||||||
|
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
|
||||||
|
if ($Token.Length -eq $Key.Length) { return $true }
|
||||||
|
return -not [char]::IsLetter($Token[$Key.Length])
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ProjectExtraArgs {
|
||||||
|
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
|
||||||
|
param([string]$Name)
|
||||||
|
$dir = (Get-Location).Path
|
||||||
|
while ($dir) {
|
||||||
|
$pf = Join-Path $dir ".v8-project.json"
|
||||||
|
if (Test-Path $pf) {
|
||||||
|
try {
|
||||||
|
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
|
||||||
|
} catch {}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
$parent = Split-Path $dir -Parent
|
||||||
|
if (-not $parent -or $parent -eq $dir) { break }
|
||||||
|
$dir = $parent
|
||||||
|
}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Assert-ExtraArgs {
|
||||||
|
# The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
# output key fails with an opaque 1C error — reject what the skill owns itself.
|
||||||
|
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
|
||||||
|
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
|
||||||
|
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
|
||||||
|
foreach ($tok in $ExtraArgs) {
|
||||||
|
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
|
||||||
|
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
foreach ($k in $owned) {
|
||||||
|
if (Test-ArgKeyMatch $tok $k) {
|
||||||
|
$hint = ''
|
||||||
|
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
|
||||||
|
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-ExtraArgs {
|
||||||
|
# Pick the argument list for the selected engine and validate it. An explicitly passed
|
||||||
|
# parameter for the other engine is an error; the same keys coming from .v8-project.json
|
||||||
|
# simply do not apply — a project may describe both engines.
|
||||||
|
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
|
||||||
|
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
|
||||||
|
# space-separated values spill into positional ones, a comma-joined list arrives as a
|
||||||
|
# single token. So accept the repo's list convention (comma-separated) and split here;
|
||||||
|
# a native array call keeps working. A value containing a comma is not supported.
|
||||||
|
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -eq 'ibcmd') {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
|
||||||
|
} else {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
|
||||||
|
}
|
||||||
|
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
|
||||||
|
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
|
||||||
|
# would nest the array — the tokens would then be glued into one argument.
|
||||||
|
return $extra
|
||||||
|
}
|
||||||
|
|
||||||
|
function Format-ArgsForDisplay {
|
||||||
|
# Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
# a leaked password does.
|
||||||
|
param([string[]]$ArgList, [string]$Engine)
|
||||||
|
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
|
||||||
|
$res = @()
|
||||||
|
$maskNext = $false
|
||||||
|
foreach ($tok in $ArgList) {
|
||||||
|
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
|
||||||
|
$hit = $null
|
||||||
|
foreach ($k in $keys) {
|
||||||
|
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
|
||||||
|
}
|
||||||
|
if (-not $hit) { $res += $tok; continue }
|
||||||
|
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
|
||||||
|
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
|
||||||
|
else { $res += ($hit + '***') }
|
||||||
|
}
|
||||||
|
return ,$res
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-CleanPath {
|
||||||
|
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
# inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
|
||||||
|
param([string]$Value, [string]$ParamName)
|
||||||
|
if (-not $Value) { return $Value }
|
||||||
|
$v = $Value.Trim()
|
||||||
|
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
|
||||||
|
$v = $v.Substring(1, $v.Length - 2).Trim()
|
||||||
|
}
|
||||||
|
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
|
||||||
|
if ($v.Contains('"')) {
|
||||||
|
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
return $v
|
||||||
|
}
|
||||||
|
|
||||||
|
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
|
||||||
|
$InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath'
|
||||||
|
$ConfigDir = ConvertTo-CleanPath $ConfigDir '-ConfigDir'
|
||||||
|
$ListFile = ConvertTo-CleanPath $ListFile '-ListFile'
|
||||||
|
|
||||||
|
function Assert-InfoBaseExists {
|
||||||
|
# These skills work on a ready infobase. Saying so up front beats the platform's
|
||||||
|
# "Неверные или отсутствующие параметры соединения" after a launch.
|
||||||
|
param([string]$Path)
|
||||||
|
if (-not $Path) { return }
|
||||||
|
if (-not (Test-Path (Join-Path $Path "1Cv8.1CD"))) {
|
||||||
|
Write-Host "Error: information base not found at $Path (no 1Cv8.1CD)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert-InfoBaseExists $InfoBasePath
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
function Find-ProjectV8Path {
|
||||||
$dir = (Get-Location).Path
|
$dir = (Get-Location).Path
|
||||||
@@ -137,7 +333,7 @@ if (-not $V8Path) {
|
|||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -146,13 +342,87 @@ if (Test-Path $V8Path -PathType Container) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
|
function ConvertFrom-PlatformBytes {
|
||||||
|
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
|
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
|
||||||
|
# one of them outright mangles Cyrillic.
|
||||||
|
param([byte[]]$Bytes)
|
||||||
|
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
|
||||||
|
try {
|
||||||
|
$strict = New-Object System.Text.UTF8Encoding($false, $true)
|
||||||
|
return $strict.GetString($Bytes)
|
||||||
|
} catch {
|
||||||
|
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-PlatformProcess {
|
||||||
|
# Run the platform non-interactively and capture its console output. A closed stdin pipe
|
||||||
|
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
|
||||||
|
# text out of our stream until we print it labelled (and out of the wrong encoding).
|
||||||
|
# Returns @{ Output; ExitCode }.
|
||||||
|
#
|
||||||
|
# Quoting differs by engine, so the caller says which it built:
|
||||||
|
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
|
||||||
|
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
|
||||||
|
# which is where 1C's own parser expects them; quoting again breaks the value.
|
||||||
|
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = if ($PreQuoted) {
|
||||||
|
$ProcArgs -join ' '
|
||||||
|
} else {
|
||||||
|
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
}
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
# stderr is drained in parallel: reading the streams one after another deadlocks
|
||||||
|
# as soon as the other one fills its pipe buffer.
|
||||||
|
$errMs = New-Object System.IO.MemoryStream
|
||||||
|
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
|
||||||
|
$outMs = New-Object System.IO.MemoryStream
|
||||||
|
$p.StandardOutput.BaseStream.CopyTo($outMs)
|
||||||
|
$errTask.Wait()
|
||||||
|
$p.WaitForExit()
|
||||||
|
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
|
||||||
|
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-PlatformOutput {
|
||||||
|
# Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
|
||||||
|
param([string]$Text)
|
||||||
|
if (-not $Text) { return }
|
||||||
|
$t = $Text.TrimEnd()
|
||||||
|
if (-not $t) { return }
|
||||||
|
$limit = 65536
|
||||||
|
if ($t.Length -gt $limit) {
|
||||||
|
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
|
||||||
|
}
|
||||||
|
Write-Host "--- Вывод платформы ---"
|
||||||
|
Write-Host $t
|
||||||
|
Write-Host "--- End ---"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' }
|
||||||
|
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if ($engine -eq "ibcmd") {
|
if ($engine -eq "ibcmd") {
|
||||||
if (-not $InfoBasePath) {
|
if (-not $InfoBasePath) {
|
||||||
@@ -216,31 +486,35 @@ try {
|
|||||||
if ($UserName) { $arguments += "--user=$UserName" }
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
if ($Password) { $arguments += "--password=$Password" }
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
$arguments += "--data=$tempDir"
|
$arguments += "--data=$tempDir"
|
||||||
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
$arguments += $extraArgs
|
||||||
$output = & $V8Path @arguments 2>&1
|
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
||||||
$exitCode = $LASTEXITCODE
|
$__ib = Invoke-PlatformProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
if ($exitCode -ne 0) {
|
if ($exitCode -ne 0) {
|
||||||
Write-Host "Error loading configuration from files (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error loading configuration from files (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||||
if ($output) { Write-Host ($output | Out-String) }
|
Write-PlatformOutput $output
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
}
|
}
|
||||||
Write-Host "Configuration loaded successfully from: $ConfigDir" -ForegroundColor Green
|
Write-Host "Configuration loaded successfully from: $ConfigDir" -ForegroundColor Green
|
||||||
if ($output) { Write-Host ($output | Out-String) }
|
Write-PlatformOutput $output
|
||||||
|
|
||||||
if ($UpdateDB) {
|
if ($UpdateDB) {
|
||||||
$applyArgs = @("infobase", "config", "apply", "--db-path=$InfoBasePath", "--force")
|
$applyArgs = @("infobase", "config", "apply", "--db-path=$InfoBasePath", "--force")
|
||||||
if ($UserName) { $applyArgs += "--user=$UserName" }
|
if ($UserName) { $applyArgs += "--user=$UserName" }
|
||||||
if ($Password) { $applyArgs += "--password=$Password" }
|
if ($Password) { $applyArgs += "--password=$Password" }
|
||||||
$applyArgs += "--data=$tempDir"
|
$applyArgs += "--data=$tempDir"
|
||||||
Write-Host "Running: ibcmd $($applyArgs -join ' ')"
|
$applyArgs += $extraArgs
|
||||||
$applyOut = & $V8Path @applyArgs 2>&1
|
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $applyArgs $engine) -join ' ') @($Password, $UserName))"
|
||||||
$exitCode = $LASTEXITCODE
|
$__ib = Invoke-PlatformProcess $V8Path $applyArgs
|
||||||
|
$applyOut = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
Write-Host "Database configuration updated successfully" -ForegroundColor Green
|
Write-Host "Database configuration updated successfully" -ForegroundColor Green
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error updating database configuration (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
if ($applyOut) { Write-Host ($applyOut | Out-String) }
|
Write-PlatformOutput $applyOut
|
||||||
}
|
}
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
}
|
}
|
||||||
@@ -319,11 +593,12 @@ try {
|
|||||||
$outFile = Join-Path $tempDir "load_log.txt"
|
$outFile = Join-Path $tempDir "load_log.txt"
|
||||||
$arguments += "/Out", "`"$outFile`""
|
$arguments += "/Out", "`"$outFile`""
|
||||||
$arguments += "/DisableStartupDialogs"
|
$arguments += "/DisableStartupDialogs"
|
||||||
|
$arguments += $extraArgs
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
|
||||||
$exitCode = $process.ExitCode
|
$exitCode = $__v8.ExitCode
|
||||||
|
|
||||||
# --- Read log ---
|
# --- Read log ---
|
||||||
$logContent = $null
|
$logContent = $null
|
||||||
@@ -362,7 +637,7 @@ try {
|
|||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
Write-Host "Load completed successfully" -ForegroundColor Green
|
Write-Host "Load completed successfully" -ForegroundColor Green
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error loading configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($logContent) {
|
if ($logContent) {
|
||||||
@@ -370,6 +645,7 @@ try {
|
|||||||
Write-Host $logContent
|
Write-Host $logContent
|
||||||
Write-Host "--- End ---"
|
Write-Host "--- End ---"
|
||||||
}
|
}
|
||||||
|
Write-PlatformOutput $__v8.Output
|
||||||
|
|
||||||
if ($silentFailures.Count -gt 0) {
|
if ($silentFailures.Count -gt 0) {
|
||||||
$msg = "[warning] log contains $($silentFailures.Count) rejection(s) — platform loaded config but dropped properties/refs"
|
$msg = "[warning] log contains $($silentFailures.Count) rejection(s) — platform loaded config but dropped properties/refs"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-load-xml v1.10 — Load 1C configuration from XML files
|
# db-load-xml v1.19 — 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
|
||||||
@@ -36,36 +36,341 @@ def _find_project_v8path():
|
|||||||
d = parent
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
V8_OWNED_KEYS = [
|
||||||
|
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
|
||||||
|
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
|
||||||
|
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
|
||||||
|
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
|
||||||
|
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||||
|
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||||
|
]
|
||||||
|
IBCMD_OWNED_KEYS = [
|
||||||
|
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||||
|
"--import", "--export", "--apply", "--force", "--create-database",
|
||||||
|
"--user", "--password",
|
||||||
|
]
|
||||||
|
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
|
||||||
|
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
|
||||||
|
|
||||||
|
|
||||||
|
def arg_key_match(token, key):
|
||||||
|
"""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
|
||||||
|
/ClearCache distinct from /C."""
|
||||||
|
if len(token) < len(key):
|
||||||
|
return False
|
||||||
|
if token[: len(key)].lower() != key.lower():
|
||||||
|
return False
|
||||||
|
if len(token) == len(key):
|
||||||
|
return True
|
||||||
|
return not token[len(key)].isalpha()
|
||||||
|
|
||||||
|
|
||||||
|
def project_extra_args(name):
|
||||||
|
"""v8args / ibcmdargs from .v8-project.json — same upward walk as v8path."""
|
||||||
|
d = os.getcwd()
|
||||||
|
while True:
|
||||||
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pf):
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
v = data.get(name)
|
||||||
|
if v:
|
||||||
|
return [str(x) for x in v]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return []
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return []
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def assert_extra_args(extra, engine, hints):
|
||||||
|
"""The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
output key fails with an opaque 1C error — reject what the skill owns itself."""
|
||||||
|
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
|
||||||
|
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
|
||||||
|
for tok in extra:
|
||||||
|
if engine == "ibcmd" and not tok.startswith("-"):
|
||||||
|
print(
|
||||||
|
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||||
|
f"({param} cannot extend the ibcmd command)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
for k in owned:
|
||||||
|
if arg_key_match(tok, k):
|
||||||
|
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||||
|
print(
|
||||||
|
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def format_args_for_display(arglist, engine):
|
||||||
|
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
a leaked password does."""
|
||||||
|
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
|
||||||
|
res = []
|
||||||
|
mask_next = False
|
||||||
|
for tok in arglist:
|
||||||
|
if mask_next:
|
||||||
|
res.append("***")
|
||||||
|
mask_next = False
|
||||||
|
continue
|
||||||
|
hit = None
|
||||||
|
for k in keys:
|
||||||
|
if tok[: len(k)].lower() == k.lower():
|
||||||
|
hit = k
|
||||||
|
break
|
||||||
|
if hit is None:
|
||||||
|
res.append(tok)
|
||||||
|
elif len(tok) == len(hit):
|
||||||
|
res.append(tok)
|
||||||
|
mask_next = True
|
||||||
|
elif tok[len(hit)] == "=":
|
||||||
|
res.append(hit + "=***")
|
||||||
|
else:
|
||||||
|
res.append(hit + "***")
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def extract_extra_args(argv, known_opts):
|
||||||
|
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
|
||||||
|
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
|
||||||
|
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
|
||||||
|
rest, v8, ibcmd = [], [], []
|
||||||
|
i = 0
|
||||||
|
while i < len(argv):
|
||||||
|
low = argv[i].lower()
|
||||||
|
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
|
||||||
|
target = v8 if low == "-additionalv8arguments" else ibcmd
|
||||||
|
i += 1
|
||||||
|
while i < len(argv) and argv[i].lower() not in known_opts:
|
||||||
|
target.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
rest.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
return rest, v8, ibcmd
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
||||||
|
"""Pick the argument list for the selected engine and validate it. An explicitly
|
||||||
|
passed parameter for the other engine is an error; the same keys coming from
|
||||||
|
.v8-project.json simply do not apply — a project may describe both engines.
|
||||||
|
|
||||||
|
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
|
||||||
|
so that form is the documented one and both ports must accept it. A value containing
|
||||||
|
a comma is not supported."""
|
||||||
|
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
|
||||||
|
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
|
||||||
|
if engine == "ibcmd" and v8_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||||
|
"(use -AdditionalIbcmdArguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine != "ibcmd" and ibcmd_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||||
|
"(use -AdditionalV8Arguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine == "ibcmd":
|
||||||
|
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
|
||||||
|
else:
|
||||||
|
extra = project_extra_args("v8args") + list(v8_extra)
|
||||||
|
if extra:
|
||||||
|
assert_extra_args(extra, engine, hints)
|
||||||
|
return extra
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
def _version_key(p):
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
"""Numeric sort key from version dir name."""
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def resolve_v8path(v8path):
|
||||||
"""Resolve path to 1cv8.exe."""
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
if not v8path:
|
if not v8path:
|
||||||
v8path = _find_project_v8path()
|
v8path = _find_project_v8path()
|
||||||
if not v8path:
|
if not v8path:
|
||||||
candidates = (
|
if os.name == "nt":
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
candidates = (
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
)
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
if candidates:
|
if candidates:
|
||||||
v8path = max(candidates, key=_version_key)
|
v8path = max(candidates, key=_version_key)
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
else:
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
if os.path.isdir(v8path):
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def decode_platform_bytes(data):
|
||||||
|
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
|
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — the locale
|
||||||
|
code page (what text=True uses) mangles both."""
|
||||||
|
if not data:
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
return data.decode("utf-8")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
return data.decode("cp866", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def assert_infobase_exists(path):
|
||||||
|
"""These skills work on a ready infobase. Saying so up front beats the platform's
|
||||||
|
"Неверные или отсутствующие параметры соединения" after a launch."""
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
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)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def clean_path(value, param=""):
|
||||||
|
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
|
||||||
|
if not value:
|
||||||
|
return value
|
||||||
|
v = value.strip()
|
||||||
|
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
|
||||||
|
v = v[1:-1].strip()
|
||||||
|
if len(v) > 3 and v[-1] in "\\/":
|
||||||
|
v = v[:-1]
|
||||||
|
if '"' in v:
|
||||||
|
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
def quote_if_needed(token):
|
||||||
|
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
|
||||||
|
verbatim, so a token with a space needs quotes of its own."""
|
||||||
|
if token and (" " in token or "\t" in token) and '"' not in token:
|
||||||
|
return f'"{token}"'
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
def run_v8(v8path, arguments):
|
||||||
|
"""Run 1cv8 in batch mode and capture its console output.
|
||||||
|
|
||||||
|
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
|
||||||
|
escape those quotes, so there the command line is handed over ready-made.
|
||||||
|
"""
|
||||||
|
if os.name == "nt":
|
||||||
|
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||||
|
else:
|
||||||
|
cmd = [v8path] + arguments
|
||||||
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def print_platform_output(result):
|
||||||
|
"""Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
|
||||||
|
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
|
||||||
|
if not text:
|
||||||
|
return
|
||||||
|
limit = 65536
|
||||||
|
if len(text) > limit:
|
||||||
|
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
|
||||||
|
print("--- Вывод платформы ---")
|
||||||
|
print(text)
|
||||||
|
print("--- End ---")
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def describe_exit(code):
|
||||||
|
"""Annotate an abnormal process exit code so a crash isn't reported as a bare number.
|
||||||
|
Batch 1C in a broken/headless environment (no GUI session, no license) can crash mid-run
|
||||||
|
instead of returning a clean error, possibly leaving the infobase locked or half-mutated."""
|
||||||
|
if code is None:
|
||||||
|
return ""
|
||||||
|
win = {
|
||||||
|
3221225477: "0xC0000005 (access violation)", -1073741819: "0xC0000005 (access violation)",
|
||||||
|
3221225781: "0xC0000135 (missing DLL)", -1073741515: "0xC0000135 (missing DLL)",
|
||||||
|
3221226505: "0xC0000409 (stack overrun)", -1073740791: "0xC0000409 (stack overrun)",
|
||||||
|
}
|
||||||
|
if code in win:
|
||||||
|
return f" — abnormal termination, exception {win[code]}; the infobase may be left in an inconsistent state; verify it before retrying"
|
||||||
|
if -64 <= code < 0:
|
||||||
|
try:
|
||||||
|
import signal
|
||||||
|
name = signal.Signals(-code).name
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
name = f"signal {-code}"
|
||||||
|
return (f" — process terminated by {name} (abnormal termination, not a normal exit); "
|
||||||
|
"the infobase may be left in an inconsistent state; verify it before retrying")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _redact(text, *secrets):
|
||||||
|
"""Redact literal secret values (password, user) from a display string —
|
||||||
|
precise, never touches lookalike paths."""
|
||||||
|
for s in secrets:
|
||||||
|
if s:
|
||||||
|
text = text.replace(s, "***")
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
@@ -102,13 +407,37 @@ def main():
|
|||||||
action="store_true",
|
action="store_true",
|
||||||
help="Treat silent rejection warnings in the log as errors (elevate exit code to 1)",
|
help="Treat silent rejection warnings in the log as errors (elevate exit code to 1)",
|
||||||
)
|
)
|
||||||
args = parser.parse_args()
|
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
|
||||||
|
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
|
||||||
|
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
|
||||||
|
help="Extra ibcmd arguments in --key=value form")
|
||||||
|
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
||||||
|
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
||||||
|
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
||||||
|
assert_infobase_exists(args.InfoBasePath)
|
||||||
|
args.ConfigDir = clean_path(args.ConfigDir, "-ConfigDir")
|
||||||
|
args.ListFile = clean_path(args.ListFile, "-ListFile")
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
|
||||||
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
arg_hints = {
|
||||||
|
"/F": "-InfoBasePath",
|
||||||
|
"/S": "-InfoBaseServer + -InfoBaseRef",
|
||||||
|
"/N": "-UserName",
|
||||||
|
"/P": "-Password",
|
||||||
|
"--db-path": "-InfoBasePath",
|
||||||
|
"--user": "-UserName",
|
||||||
|
"--password": "-Password",
|
||||||
|
}
|
||||||
|
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if engine == "ibcmd":
|
if engine == "ibcmd":
|
||||||
if not args.InfoBasePath:
|
if not args.InfoBasePath:
|
||||||
@@ -166,18 +495,13 @@ def main():
|
|||||||
if args.Password:
|
if args.Password:
|
||||||
arguments.append(f"--password={args.Password}")
|
arguments.append(f"--password={args.Password}")
|
||||||
arguments.append(f"--data={ib_data}")
|
arguments.append(f"--data={ib_data}")
|
||||||
print(f"Running: ibcmd {' '.join(arguments)}")
|
arguments.extend(extra_args)
|
||||||
result = subprocess.run([v8path] + arguments, capture_output=True, encoding="utf-8", errors="replace")
|
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, 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})", file=sys.stderr)
|
print(f"Error loading configuration from files (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
|
||||||
if result.stdout:
|
|
||||||
print(result.stdout)
|
|
||||||
if result.stderr:
|
|
||||||
print(result.stderr, file=sys.stderr)
|
|
||||||
sys.exit(result.returncode)
|
sys.exit(result.returncode)
|
||||||
print(f"Configuration loaded successfully from: {args.ConfigDir}")
|
print(f"Configuration loaded successfully from: {args.ConfigDir}")
|
||||||
if result.stdout:
|
|
||||||
print(result.stdout)
|
|
||||||
exit_code = 0
|
exit_code = 0
|
||||||
if args.UpdateDB:
|
if args.UpdateDB:
|
||||||
apply_args = ["infobase", "config", "apply", f"--db-path={args.InfoBasePath}", "--force"]
|
apply_args = ["infobase", "config", "apply", f"--db-path={args.InfoBasePath}", "--force"]
|
||||||
@@ -186,17 +510,15 @@ def main():
|
|||||||
if args.Password:
|
if args.Password:
|
||||||
apply_args.append(f"--password={args.Password}")
|
apply_args.append(f"--password={args.Password}")
|
||||||
apply_args.append(f"--data={ib_data}")
|
apply_args.append(f"--data={ib_data}")
|
||||||
print(f"Running: ibcmd {' '.join(apply_args)}")
|
apply_args.extend(extra_args)
|
||||||
ar = subprocess.run([v8path] + apply_args, capture_output=True, encoding="utf-8", errors="replace")
|
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(apply_args, engine)), args.Password, args.UserName)}")
|
||||||
|
ar = run_ibcmd([v8path] + apply_args, bool(args.UserName))
|
||||||
exit_code = ar.returncode
|
exit_code = ar.returncode
|
||||||
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})", file=sys.stderr)
|
print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
|
||||||
if ar.stdout:
|
print_platform_output(ar)
|
||||||
print(ar.stdout)
|
|
||||||
if ar.stderr:
|
|
||||||
print(ar.stderr, file=sys.stderr)
|
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
# --- Temp dir ---
|
# --- Temp dir ---
|
||||||
@@ -208,16 +530,16 @@ def main():
|
|||||||
arguments = ["DESIGNER"]
|
arguments = ["DESIGNER"]
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
if args.InfoBaseServer and args.InfoBaseRef:
|
||||||
arguments += ["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"]
|
arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
|
||||||
else:
|
else:
|
||||||
arguments += ["/F", args.InfoBasePath]
|
arguments += ["/F", f'"{args.InfoBasePath}"']
|
||||||
|
|
||||||
if args.UserName:
|
if args.UserName:
|
||||||
arguments.append(f"/N{args.UserName}")
|
arguments.append(f'/N"{args.UserName}"')
|
||||||
if args.Password:
|
if args.Password:
|
||||||
arguments.append(f"/P{args.Password}")
|
arguments.append(f'/P"{args.Password}"')
|
||||||
|
|
||||||
arguments += ["/LoadConfigFromFiles", args.ConfigDir]
|
arguments += ["/LoadConfigFromFiles", f'"{args.ConfigDir}"']
|
||||||
|
|
||||||
if args.Mode == "Full":
|
if args.Mode == "Full":
|
||||||
print("Executing full configuration load...")
|
print("Executing full configuration load...")
|
||||||
@@ -253,7 +575,7 @@ def main():
|
|||||||
for fl in file_list:
|
for fl in file_list:
|
||||||
print(f" {fl}")
|
print(f" {fl}")
|
||||||
|
|
||||||
arguments += ["-listFile", generated_list_file]
|
arguments += ["-listFile", f'"{generated_list_file}"']
|
||||||
arguments.append("-partial")
|
arguments.append("-partial")
|
||||||
arguments.append("-updateConfigDumpInfo")
|
arguments.append("-updateConfigDumpInfo")
|
||||||
|
|
||||||
@@ -261,7 +583,7 @@ def main():
|
|||||||
|
|
||||||
# --- Extensions ---
|
# --- Extensions ---
|
||||||
if args.Extension:
|
if args.Extension:
|
||||||
arguments += ["-Extension", args.Extension]
|
arguments += ["-Extension", f'"{args.Extension}"']
|
||||||
elif args.AllExtensions:
|
elif args.AllExtensions:
|
||||||
arguments.append("-AllExtensions")
|
arguments.append("-AllExtensions")
|
||||||
|
|
||||||
@@ -271,16 +593,13 @@ def main():
|
|||||||
|
|
||||||
# --- Output ---
|
# --- Output ---
|
||||||
out_file = os.path.join(temp_dir, "load_log.txt")
|
out_file = os.path.join(temp_dir, "load_log.txt")
|
||||||
arguments += ["/Out", out_file]
|
arguments += ["/Out", f'"{out_file}"']
|
||||||
arguments.append("/DisableStartupDialogs")
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
arguments.extend(quote_if_needed(a) for a in extra_args)
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||||
result = subprocess.run(
|
result = run_v8(v8path, arguments)
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
exit_code = result.returncode
|
||||||
|
|
||||||
# --- Read log ---
|
# --- Read log ---
|
||||||
@@ -319,13 +638,14 @@ 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})", file=sys.stderr)
|
print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
|
||||||
|
|
||||||
if log_content:
|
if log_content:
|
||||||
print("--- Log ---")
|
print("--- Log ---")
|
||||||
print(log_content)
|
print(log_content)
|
||||||
print("--- End ---")
|
print("--- End ---")
|
||||||
|
|
||||||
|
print_platform_output(result)
|
||||||
if silent_failures:
|
if silent_failures:
|
||||||
suffix = "" if args.StrictLog else " (pass -StrictLog to treat as error)"
|
suffix = "" if args.StrictLog else " (pass -StrictLog to treat as error)"
|
||||||
print(
|
print(
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" <пар
|
|||||||
| `-Execute <файл.epf>` | нет | Запуск внешней обработки сразу после старта |
|
| `-Execute <файл.epf>` | нет | Запуск внешней обработки сразу после старта |
|
||||||
| `-CParam <строка>` | нет | Параметр запуска (/C) |
|
| `-CParam <строка>` | нет | Параметр запуска (/C) |
|
||||||
| `-URL <ссылка>` | нет | Навигационная ссылка (формат `e1cib/...`) |
|
| `-URL <ссылка>` | нет | Навигационная ссылка (формат `e1cib/...`) |
|
||||||
|
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
|
||||||
|
|
||||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# db-run v1.1 — Launch 1C:Enterprise
|
# db-run v1.7 — 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 не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Запуск 1С:Предприятие
|
Запуск 1С:Предприятие
|
||||||
@@ -35,6 +36,12 @@
|
|||||||
.PARAMETER URL
|
.PARAMETER URL
|
||||||
Навигационная ссылка (e1cib/...)
|
Навигационная ссылка (e1cib/...)
|
||||||
|
|
||||||
|
.PARAMETER AdditionalV8Arguments
|
||||||
|
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
|
||||||
|
|
||||||
|
.PARAMETER AdditionalIbcmdArguments
|
||||||
|
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
|
||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
.\db-run.ps1 -InfoBasePath "C:\Bases\MyDB"
|
.\db-run.ps1 -InfoBasePath "C:\Bases\MyDB"
|
||||||
|
|
||||||
@@ -72,12 +79,170 @@ param(
|
|||||||
[string]$CParam,
|
[string]$CParam,
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[string]$URL
|
[string]$URL,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalV8Arguments = @(),
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalIbcmdArguments = @()
|
||||||
)
|
)
|
||||||
|
|
||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
function Protect-Secrets {
|
||||||
|
# Redact literal secret values from a display string (String.Replace is literal, not regex).
|
||||||
|
param([string]$Text, [string[]]$Secrets)
|
||||||
|
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
|
||||||
|
return $Text
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
$script:V8OwnedKeys = @(
|
||||||
|
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
|
||||||
|
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
|
||||||
|
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
|
||||||
|
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
|
||||||
|
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||||
|
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||||
|
)
|
||||||
|
$script:IbcmdOwnedKeys = @(
|
||||||
|
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||||
|
'--import', '--export', '--apply', '--force', '--create-database',
|
||||||
|
'--user', '--password'
|
||||||
|
)
|
||||||
|
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
|
||||||
|
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
|
||||||
|
|
||||||
|
function Test-ArgKeyMatch {
|
||||||
|
# A token matches a key when it equals the key, or starts with it and the next
|
||||||
|
# character is not a letter — catches glued /N"user" and --password=x, while
|
||||||
|
# keeping /ClearCache distinct from /C.
|
||||||
|
param([string]$Token, [string]$Key)
|
||||||
|
if ($Token.Length -lt $Key.Length) { return $false }
|
||||||
|
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
|
||||||
|
if ($Token.Length -eq $Key.Length) { return $true }
|
||||||
|
return -not [char]::IsLetter($Token[$Key.Length])
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ProjectExtraArgs {
|
||||||
|
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
|
||||||
|
param([string]$Name)
|
||||||
|
$dir = (Get-Location).Path
|
||||||
|
while ($dir) {
|
||||||
|
$pf = Join-Path $dir ".v8-project.json"
|
||||||
|
if (Test-Path $pf) {
|
||||||
|
try {
|
||||||
|
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
|
||||||
|
} catch {}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
$parent = Split-Path $dir -Parent
|
||||||
|
if (-not $parent -or $parent -eq $dir) { break }
|
||||||
|
$dir = $parent
|
||||||
|
}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Assert-ExtraArgs {
|
||||||
|
# The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
# output key fails with an opaque 1C error — reject what the skill owns itself.
|
||||||
|
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
|
||||||
|
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
|
||||||
|
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
|
||||||
|
foreach ($tok in $ExtraArgs) {
|
||||||
|
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
|
||||||
|
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
foreach ($k in $owned) {
|
||||||
|
if (Test-ArgKeyMatch $tok $k) {
|
||||||
|
$hint = ''
|
||||||
|
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
|
||||||
|
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-ExtraArgs {
|
||||||
|
# Pick the argument list for the selected engine and validate it. An explicitly passed
|
||||||
|
# parameter for the other engine is an error; the same keys coming from .v8-project.json
|
||||||
|
# simply do not apply — a project may describe both engines.
|
||||||
|
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
|
||||||
|
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
|
||||||
|
# space-separated values spill into positional ones, a comma-joined list arrives as a
|
||||||
|
# single token. So accept the repo's list convention (comma-separated) and split here;
|
||||||
|
# a native array call keeps working. A value containing a comma is not supported.
|
||||||
|
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -eq 'ibcmd') {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
|
||||||
|
} else {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
|
||||||
|
}
|
||||||
|
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
|
||||||
|
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
|
||||||
|
# would nest the array — the tokens would then be glued into one argument.
|
||||||
|
return $extra
|
||||||
|
}
|
||||||
|
|
||||||
|
function Format-ArgsForDisplay {
|
||||||
|
# Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
# a leaked password does.
|
||||||
|
param([string[]]$ArgList, [string]$Engine)
|
||||||
|
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
|
||||||
|
$res = @()
|
||||||
|
$maskNext = $false
|
||||||
|
foreach ($tok in $ArgList) {
|
||||||
|
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
|
||||||
|
$hit = $null
|
||||||
|
foreach ($k in $keys) {
|
||||||
|
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
|
||||||
|
}
|
||||||
|
if (-not $hit) { $res += $tok; continue }
|
||||||
|
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
|
||||||
|
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
|
||||||
|
else { $res += ($hit + '***') }
|
||||||
|
}
|
||||||
|
return ,$res
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-CleanPath {
|
||||||
|
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
# inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
|
||||||
|
param([string]$Value, [string]$ParamName)
|
||||||
|
if (-not $Value) { return $Value }
|
||||||
|
$v = $Value.Trim()
|
||||||
|
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
|
||||||
|
$v = $v.Substring(1, $v.Length - 2).Trim()
|
||||||
|
}
|
||||||
|
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
|
||||||
|
if ($v.Contains('"')) {
|
||||||
|
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
return $v
|
||||||
|
}
|
||||||
|
|
||||||
|
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
|
||||||
|
$InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath'
|
||||||
|
$Execute = ConvertTo-CleanPath $Execute '-Execute'
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
function Find-ProjectV8Path {
|
||||||
$dir = (Get-Location).Path
|
$dir = (Get-Location).Path
|
||||||
@@ -108,7 +273,7 @@ if (-not $V8Path) {
|
|||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,10 +282,23 @@ if (Test-Path $V8Path -PathType Container) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Resolve additional arguments ---
|
||||||
|
# 1C:Enterprise is always launched by 1cv8 — ibcmd has no interactive mode.
|
||||||
|
$engine = "1cv8"
|
||||||
|
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '/Execute' = '-Execute'; '/C' = '-CParam'; '/URL' = '-URL' }
|
||||||
|
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
|
||||||
|
|
||||||
|
function Format-ArgToken {
|
||||||
|
# ShellExecute re-joins the argument string, so quote each extra token that needs it.
|
||||||
|
param([string]$Token)
|
||||||
|
if ($Token -match '[\s"]') { return ' "' + ($Token -replace '"', '\"') + '"' }
|
||||||
|
return " $Token"
|
||||||
|
}
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||||
@@ -164,7 +342,28 @@ if ($URL) {
|
|||||||
|
|
||||||
$argString += " /DisableStartupDialogs"
|
$argString += " /DisableStartupDialogs"
|
||||||
|
|
||||||
# --- Execute (background, no wait) ---
|
# The display string is built from the same tokens with secret-prone values redacted.
|
||||||
Write-Host "Running: 1cv8.exe $argString"
|
$displayString = $argString
|
||||||
Start-Process -FilePath $V8Path -ArgumentList $argString
|
foreach ($tok in $extraArgs) { $argString += (Format-ArgToken $tok) }
|
||||||
|
foreach ($tok in (Format-ArgsForDisplay $extraArgs $engine)) { $displayString += (Format-ArgToken $tok) }
|
||||||
|
|
||||||
|
# --- Execute (background) ---
|
||||||
|
# Redact the password/user before printing the command line — never leak secrets.
|
||||||
|
$displayArg = Protect-Secrets $displayString @($Password, $UserName)
|
||||||
|
Write-Host "Running: 1cv8.exe $displayArg"
|
||||||
|
$proc = Start-Process -FilePath $V8Path -ArgumentList $argString -PassThru
|
||||||
|
|
||||||
|
# --- Bounded early-exit check ---
|
||||||
|
# The launch is a background GUI process, so we don't wait for completion. But a process
|
||||||
|
# that dies within the first ~1.5s never really started (bad base, no display, license) —
|
||||||
|
# report that honestly instead of a blind "launched".
|
||||||
|
$deadline = (Get-Date).AddMilliseconds(1500)
|
||||||
|
while ((Get-Date) -lt $deadline -and -not $proc.HasExited) {
|
||||||
|
Start-Sleep -Milliseconds 200
|
||||||
|
}
|
||||||
|
if ($proc.HasExited) {
|
||||||
|
Write-Host "Error: 1C:Enterprise exited immediately (code: $($proc.ExitCode))" -ForegroundColor Red
|
||||||
|
if ($proc.ExitCode -ne 0) { exit $proc.ExitCode } else { exit 1 }
|
||||||
|
}
|
||||||
|
Write-Host "PID: $($proc.Id)"
|
||||||
Write-Host "1C:Enterprise launched" -ForegroundColor Green
|
Write-Host "1C:Enterprise launched" -ForegroundColor Green
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-run v1.1 — Launch 1C:Enterprise
|
# db-run v1.7 — 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
|
||||||
@@ -9,6 +9,7 @@ import os
|
|||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
def _find_project_v8path():
|
||||||
@@ -32,36 +33,232 @@ def _find_project_v8path():
|
|||||||
d = parent
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
V8_OWNED_KEYS = [
|
||||||
|
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
|
||||||
|
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
|
||||||
|
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
|
||||||
|
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
|
||||||
|
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||||
|
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||||
|
]
|
||||||
|
IBCMD_OWNED_KEYS = [
|
||||||
|
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||||
|
"--import", "--export", "--apply", "--force", "--create-database",
|
||||||
|
"--user", "--password",
|
||||||
|
]
|
||||||
|
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
|
||||||
|
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
|
||||||
|
|
||||||
|
|
||||||
|
def arg_key_match(token, key):
|
||||||
|
"""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
|
||||||
|
/ClearCache distinct from /C."""
|
||||||
|
if len(token) < len(key):
|
||||||
|
return False
|
||||||
|
if token[: len(key)].lower() != key.lower():
|
||||||
|
return False
|
||||||
|
if len(token) == len(key):
|
||||||
|
return True
|
||||||
|
return not token[len(key)].isalpha()
|
||||||
|
|
||||||
|
|
||||||
|
def project_extra_args(name):
|
||||||
|
"""v8args / ibcmdargs from .v8-project.json — same upward walk as v8path."""
|
||||||
|
d = os.getcwd()
|
||||||
|
while True:
|
||||||
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pf):
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
v = data.get(name)
|
||||||
|
if v:
|
||||||
|
return [str(x) for x in v]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return []
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return []
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def assert_extra_args(extra, engine, hints):
|
||||||
|
"""The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
output key fails with an opaque 1C error — reject what the skill owns itself."""
|
||||||
|
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
|
||||||
|
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
|
||||||
|
for tok in extra:
|
||||||
|
if engine == "ibcmd" and not tok.startswith("-"):
|
||||||
|
print(
|
||||||
|
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||||
|
f"({param} cannot extend the ibcmd command)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
for k in owned:
|
||||||
|
if arg_key_match(tok, k):
|
||||||
|
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||||
|
print(
|
||||||
|
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def format_args_for_display(arglist, engine):
|
||||||
|
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
a leaked password does."""
|
||||||
|
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
|
||||||
|
res = []
|
||||||
|
mask_next = False
|
||||||
|
for tok in arglist:
|
||||||
|
if mask_next:
|
||||||
|
res.append("***")
|
||||||
|
mask_next = False
|
||||||
|
continue
|
||||||
|
hit = None
|
||||||
|
for k in keys:
|
||||||
|
if tok[: len(k)].lower() == k.lower():
|
||||||
|
hit = k
|
||||||
|
break
|
||||||
|
if hit is None:
|
||||||
|
res.append(tok)
|
||||||
|
elif len(tok) == len(hit):
|
||||||
|
res.append(tok)
|
||||||
|
mask_next = True
|
||||||
|
elif tok[len(hit)] == "=":
|
||||||
|
res.append(hit + "=***")
|
||||||
|
else:
|
||||||
|
res.append(hit + "***")
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def extract_extra_args(argv, known_opts):
|
||||||
|
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
|
||||||
|
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
|
||||||
|
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
|
||||||
|
rest, v8, ibcmd = [], [], []
|
||||||
|
i = 0
|
||||||
|
while i < len(argv):
|
||||||
|
low = argv[i].lower()
|
||||||
|
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
|
||||||
|
target = v8 if low == "-additionalv8arguments" else ibcmd
|
||||||
|
i += 1
|
||||||
|
while i < len(argv) and argv[i].lower() not in known_opts:
|
||||||
|
target.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
rest.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
return rest, v8, ibcmd
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
||||||
|
"""Pick the argument list for the selected engine and validate it. An explicitly
|
||||||
|
passed parameter for the other engine is an error; the same keys coming from
|
||||||
|
.v8-project.json simply do not apply — a project may describe both engines.
|
||||||
|
|
||||||
|
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
|
||||||
|
so that form is the documented one and both ports must accept it. A value containing
|
||||||
|
a comma is not supported."""
|
||||||
|
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
|
||||||
|
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
|
||||||
|
if engine == "ibcmd" and v8_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||||
|
"(use -AdditionalIbcmdArguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine != "ibcmd" and ibcmd_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||||
|
"(use -AdditionalV8Arguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine == "ibcmd":
|
||||||
|
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
|
||||||
|
else:
|
||||||
|
extra = project_extra_args("v8args") + list(v8_extra)
|
||||||
|
if extra:
|
||||||
|
assert_extra_args(extra, engine, hints)
|
||||||
|
return extra
|
||||||
|
|
||||||
|
|
||||||
|
def clean_path(value, param=""):
|
||||||
|
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
|
||||||
|
if not value:
|
||||||
|
return value
|
||||||
|
v = value.strip()
|
||||||
|
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
|
||||||
|
v = v[1:-1].strip()
|
||||||
|
if len(v) > 3 and v[-1] in "\\/":
|
||||||
|
v = v[:-1]
|
||||||
|
if '"' in v:
|
||||||
|
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
def _version_key(p):
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
"""Numeric sort key from version dir name."""
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def resolve_v8path(v8path):
|
||||||
"""Resolve path to 1cv8.exe."""
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
if not v8path:
|
if not v8path:
|
||||||
v8path = _find_project_v8path()
|
v8path = _find_project_v8path()
|
||||||
if not v8path:
|
if not v8path:
|
||||||
candidates = (
|
if os.name == "nt":
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
candidates = (
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
)
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
if candidates:
|
if candidates:
|
||||||
v8path = max(candidates, key=_version_key)
|
v8path = max(candidates, key=_version_key)
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
else:
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
if os.path.isdir(v8path):
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
def _redact(text, *secrets):
|
||||||
|
"""Redact literal secret values (password, user) from a display string —
|
||||||
|
precise, never touches lookalike paths."""
|
||||||
|
for s in secrets:
|
||||||
|
if s:
|
||||||
|
text = text.replace(s, "***")
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
@@ -78,10 +275,34 @@ def main():
|
|||||||
parser.add_argument("-Execute", default="")
|
parser.add_argument("-Execute", default="")
|
||||||
parser.add_argument("-CParam", default="")
|
parser.add_argument("-CParam", default="")
|
||||||
parser.add_argument("-URL", default="")
|
parser.add_argument("-URL", default="")
|
||||||
args = parser.parse_args()
|
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
|
||||||
|
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
|
||||||
|
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
|
||||||
|
help="Extra ibcmd arguments in --key=value form")
|
||||||
|
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
||||||
|
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
||||||
|
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
||||||
|
args.Execute = clean_path(args.Execute, "-Execute")
|
||||||
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
|
||||||
|
# --- Resolve additional arguments ---
|
||||||
|
# 1C:Enterprise is always launched by 1cv8 — ibcmd has no interactive mode.
|
||||||
|
engine = "1cv8"
|
||||||
|
arg_hints = {
|
||||||
|
"/F": "-InfoBasePath",
|
||||||
|
"/S": "-InfoBaseServer + -InfoBaseRef",
|
||||||
|
"/N": "-UserName",
|
||||||
|
"/P": "-Password",
|
||||||
|
"/Execute": "-Execute",
|
||||||
|
"/C": "-CParam",
|
||||||
|
"/URL": "-URL",
|
||||||
|
}
|
||||||
|
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
|
||||||
|
|
||||||
# --- 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", file=sys.stderr)
|
||||||
@@ -118,10 +339,25 @@ def main():
|
|||||||
arguments.extend(["/URL", args.URL])
|
arguments.extend(["/URL", args.URL])
|
||||||
|
|
||||||
arguments.append("/DisableStartupDialogs")
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
arguments.extend(extra_args)
|
||||||
|
|
||||||
# --- Execute (background, no wait) ---
|
# --- Execute (background) ---
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
# Redact the password/user before printing the command line — never leak secrets.
|
||||||
subprocess.Popen([v8path] + arguments)
|
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||||
|
proc = subprocess.Popen([v8path] + arguments)
|
||||||
|
|
||||||
|
# --- Bounded early-exit check ---
|
||||||
|
# The launch is a background GUI process, so we don't wait for completion. But a process
|
||||||
|
# that dies within the first ~1.5s never really started (bad base, no display, license) —
|
||||||
|
# report that honestly instead of a blind "launched".
|
||||||
|
deadline = time.monotonic() + 1.5
|
||||||
|
while time.monotonic() < deadline and proc.poll() is None:
|
||||||
|
time.sleep(0.2)
|
||||||
|
rc = proc.poll()
|
||||||
|
if rc is not None:
|
||||||
|
print(f"Error: 1C:Enterprise exited immediately (code: {rc})", file=sys.stderr)
|
||||||
|
sys.exit(rc if rc and rc > 0 else 1)
|
||||||
|
print(f"PID: {proc.pid}")
|
||||||
print("1C:Enterprise launched")
|
print("1C:Enterprise launched")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" <п
|
|||||||
| `-Dynamic <+/->` | нет | `+` — динамическое обновление, `-` — отключить |
|
| `-Dynamic <+/->` | нет | `+` — динамическое обновление, `-` — отключить |
|
||||||
| `-Server` | нет | Обновление на стороне сервера |
|
| `-Server` | нет | Обновление на стороне сервера |
|
||||||
| `-WarningsAsErrors` | нет | Предупреждения считать ошибками |
|
| `-WarningsAsErrors` | нет | Предупреждения считать ошибками |
|
||||||
|
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
|
||||||
|
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
|
||||||
|
|
||||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# db-update v1.4 — Update 1C database configuration
|
# db-update v1.13 — 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 не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Обновление конфигурации базы данных 1С
|
Обновление конфигурации базы данных 1С
|
||||||
@@ -41,6 +42,12 @@
|
|||||||
.PARAMETER WarningsAsErrors
|
.PARAMETER WarningsAsErrors
|
||||||
Предупреждения считать ошибками
|
Предупреждения считать ошибками
|
||||||
|
|
||||||
|
.PARAMETER AdditionalV8Arguments
|
||||||
|
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
|
||||||
|
|
||||||
|
.PARAMETER AdditionalIbcmdArguments
|
||||||
|
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
|
||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
.\db-update.ps1 -InfoBasePath "C:\Bases\MyDB"
|
.\db-update.ps1 -InfoBasePath "C:\Bases\MyDB"
|
||||||
|
|
||||||
@@ -82,12 +89,199 @@ param(
|
|||||||
[switch]$Server,
|
[switch]$Server,
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[switch]$WarningsAsErrors
|
[switch]$WarningsAsErrors,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalV8Arguments = @(),
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalIbcmdArguments = @()
|
||||||
)
|
)
|
||||||
|
|
||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
function Protect-Secrets {
|
||||||
|
# Redact literal secret values from a display string (String.Replace is literal, not regex).
|
||||||
|
param([string]$Text, [string[]]$Secrets)
|
||||||
|
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
|
||||||
|
return $Text
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ExitAnnotation {
|
||||||
|
# Annotate an abnormal process exit code so a crash isn't reported as a bare number.
|
||||||
|
# A batch DESIGNER that crashes (e.g. missing license) may leave the infobase locked or
|
||||||
|
# half-updated — surface that instead of a plain code. (Windows exception codes only;
|
||||||
|
# POSIX signals are handled in the .py port.)
|
||||||
|
param([int]$Code)
|
||||||
|
$win = @{
|
||||||
|
-1073741819 = "0xC0000005 (access violation)"
|
||||||
|
-1073741515 = "0xC0000135 (missing DLL)"
|
||||||
|
-1073740791 = "0xC0000409 (stack overrun)"
|
||||||
|
}
|
||||||
|
if ($win.ContainsKey($Code)) {
|
||||||
|
return " — abnormal termination, exception $($win[$Code]); the infobase may be left in an inconsistent state; verify it before retrying"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
$script:V8OwnedKeys = @(
|
||||||
|
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
|
||||||
|
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
|
||||||
|
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
|
||||||
|
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
|
||||||
|
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||||
|
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||||
|
)
|
||||||
|
$script:IbcmdOwnedKeys = @(
|
||||||
|
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||||
|
'--import', '--export', '--apply', '--force', '--create-database',
|
||||||
|
'--user', '--password'
|
||||||
|
)
|
||||||
|
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
|
||||||
|
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
|
||||||
|
|
||||||
|
function Test-ArgKeyMatch {
|
||||||
|
# A token matches a key when it equals the key, or starts with it and the next
|
||||||
|
# character is not a letter — catches glued /N"user" and --password=x, while
|
||||||
|
# keeping /ClearCache distinct from /C.
|
||||||
|
param([string]$Token, [string]$Key)
|
||||||
|
if ($Token.Length -lt $Key.Length) { return $false }
|
||||||
|
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
|
||||||
|
if ($Token.Length -eq $Key.Length) { return $true }
|
||||||
|
return -not [char]::IsLetter($Token[$Key.Length])
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ProjectExtraArgs {
|
||||||
|
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
|
||||||
|
param([string]$Name)
|
||||||
|
$dir = (Get-Location).Path
|
||||||
|
while ($dir) {
|
||||||
|
$pf = Join-Path $dir ".v8-project.json"
|
||||||
|
if (Test-Path $pf) {
|
||||||
|
try {
|
||||||
|
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
|
||||||
|
} catch {}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
$parent = Split-Path $dir -Parent
|
||||||
|
if (-not $parent -or $parent -eq $dir) { break }
|
||||||
|
$dir = $parent
|
||||||
|
}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Assert-ExtraArgs {
|
||||||
|
# The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
# output key fails with an opaque 1C error — reject what the skill owns itself.
|
||||||
|
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
|
||||||
|
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
|
||||||
|
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
|
||||||
|
foreach ($tok in $ExtraArgs) {
|
||||||
|
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
|
||||||
|
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
foreach ($k in $owned) {
|
||||||
|
if (Test-ArgKeyMatch $tok $k) {
|
||||||
|
$hint = ''
|
||||||
|
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
|
||||||
|
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-ExtraArgs {
|
||||||
|
# Pick the argument list for the selected engine and validate it. An explicitly passed
|
||||||
|
# parameter for the other engine is an error; the same keys coming from .v8-project.json
|
||||||
|
# simply do not apply — a project may describe both engines.
|
||||||
|
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
|
||||||
|
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
|
||||||
|
# space-separated values spill into positional ones, a comma-joined list arrives as a
|
||||||
|
# single token. So accept the repo's list convention (comma-separated) and split here;
|
||||||
|
# a native array call keeps working. A value containing a comma is not supported.
|
||||||
|
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -eq 'ibcmd') {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
|
||||||
|
} else {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
|
||||||
|
}
|
||||||
|
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
|
||||||
|
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
|
||||||
|
# would nest the array — the tokens would then be glued into one argument.
|
||||||
|
return $extra
|
||||||
|
}
|
||||||
|
|
||||||
|
function Format-ArgsForDisplay {
|
||||||
|
# Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
# a leaked password does.
|
||||||
|
param([string[]]$ArgList, [string]$Engine)
|
||||||
|
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
|
||||||
|
$res = @()
|
||||||
|
$maskNext = $false
|
||||||
|
foreach ($tok in $ArgList) {
|
||||||
|
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
|
||||||
|
$hit = $null
|
||||||
|
foreach ($k in $keys) {
|
||||||
|
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
|
||||||
|
}
|
||||||
|
if (-not $hit) { $res += $tok; continue }
|
||||||
|
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
|
||||||
|
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
|
||||||
|
else { $res += ($hit + '***') }
|
||||||
|
}
|
||||||
|
return ,$res
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-CleanPath {
|
||||||
|
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
# inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
|
||||||
|
param([string]$Value, [string]$ParamName)
|
||||||
|
if (-not $Value) { return $Value }
|
||||||
|
$v = $Value.Trim()
|
||||||
|
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
|
||||||
|
$v = $v.Substring(1, $v.Length - 2).Trim()
|
||||||
|
}
|
||||||
|
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
|
||||||
|
if ($v.Contains('"')) {
|
||||||
|
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
return $v
|
||||||
|
}
|
||||||
|
|
||||||
|
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
|
||||||
|
$InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath'
|
||||||
|
|
||||||
|
function Assert-InfoBaseExists {
|
||||||
|
# These skills work on a ready infobase. Saying so up front beats the platform's
|
||||||
|
# "Неверные или отсутствующие параметры соединения" after a launch.
|
||||||
|
param([string]$Path)
|
||||||
|
if (-not $Path) { return }
|
||||||
|
if (-not (Test-Path (Join-Path $Path "1Cv8.1CD"))) {
|
||||||
|
Write-Host "Error: information base not found at $Path (no 1Cv8.1CD)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert-InfoBaseExists $InfoBasePath
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
function Find-ProjectV8Path {
|
||||||
$dir = (Get-Location).Path
|
$dir = (Get-Location).Path
|
||||||
@@ -118,7 +312,7 @@ if (-not $V8Path) {
|
|||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,13 +321,87 @@ if (Test-Path $V8Path -PathType Container) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
|
function ConvertFrom-PlatformBytes {
|
||||||
|
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
|
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
|
||||||
|
# one of them outright mangles Cyrillic.
|
||||||
|
param([byte[]]$Bytes)
|
||||||
|
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
|
||||||
|
try {
|
||||||
|
$strict = New-Object System.Text.UTF8Encoding($false, $true)
|
||||||
|
return $strict.GetString($Bytes)
|
||||||
|
} catch {
|
||||||
|
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-PlatformProcess {
|
||||||
|
# Run the platform non-interactively and capture its console output. A closed stdin pipe
|
||||||
|
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
|
||||||
|
# text out of our stream until we print it labelled (and out of the wrong encoding).
|
||||||
|
# Returns @{ Output; ExitCode }.
|
||||||
|
#
|
||||||
|
# Quoting differs by engine, so the caller says which it built:
|
||||||
|
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
|
||||||
|
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
|
||||||
|
# which is where 1C's own parser expects them; quoting again breaks the value.
|
||||||
|
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = if ($PreQuoted) {
|
||||||
|
$ProcArgs -join ' '
|
||||||
|
} else {
|
||||||
|
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
}
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
# stderr is drained in parallel: reading the streams one after another deadlocks
|
||||||
|
# as soon as the other one fills its pipe buffer.
|
||||||
|
$errMs = New-Object System.IO.MemoryStream
|
||||||
|
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
|
||||||
|
$outMs = New-Object System.IO.MemoryStream
|
||||||
|
$p.StandardOutput.BaseStream.CopyTo($outMs)
|
||||||
|
$errTask.Wait()
|
||||||
|
$p.WaitForExit()
|
||||||
|
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
|
||||||
|
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-PlatformOutput {
|
||||||
|
# Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
|
||||||
|
param([string]$Text)
|
||||||
|
if (-not $Text) { return }
|
||||||
|
$t = $Text.TrimEnd()
|
||||||
|
if (-not $t) { return }
|
||||||
|
$limit = 65536
|
||||||
|
if ($t.Length -gt $limit) {
|
||||||
|
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
|
||||||
|
}
|
||||||
|
Write-Host "--- Вывод платформы ---"
|
||||||
|
Write-Host $t
|
||||||
|
Write-Host "--- End ---"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' }
|
||||||
|
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if ($engine -eq "ibcmd") {
|
if ($engine -eq "ibcmd") {
|
||||||
if (-not $InfoBasePath) {
|
if (-not $InfoBasePath) {
|
||||||
@@ -163,15 +431,17 @@ try {
|
|||||||
if ($UserName) { $arguments += "--user=$UserName" }
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
if ($Password) { $arguments += "--password=$Password" }
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
$arguments += "--data=$tempDir"
|
$arguments += "--data=$tempDir"
|
||||||
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
$arguments += $extraArgs
|
||||||
$output = & $V8Path @arguments 2>&1
|
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
||||||
$exitCode = $LASTEXITCODE
|
$__ib = Invoke-PlatformProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
Write-Host "Database configuration updated successfully" -ForegroundColor Green
|
Write-Host "Database configuration updated successfully" -ForegroundColor Green
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error updating database configuration (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
if ($output) { Write-Host ($output | Out-String) }
|
Write-PlatformOutput $output
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,17 +482,18 @@ try {
|
|||||||
$outFile = Join-Path $tempDir "update_log.txt"
|
$outFile = Join-Path $tempDir "update_log.txt"
|
||||||
$arguments += "/Out", "`"$outFile`""
|
$arguments += "/Out", "`"$outFile`""
|
||||||
$arguments += "/DisableStartupDialogs"
|
$arguments += "/DisableStartupDialogs"
|
||||||
|
$arguments += $extraArgs
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
|
||||||
$exitCode = $process.ExitCode
|
$exitCode = $__v8.ExitCode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
Write-Host "Database configuration updated successfully" -ForegroundColor Green
|
Write-Host "Database configuration updated successfully" -ForegroundColor Green
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error updating database configuration (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Test-Path $outFile) {
|
if (Test-Path $outFile) {
|
||||||
@@ -233,6 +504,7 @@ try {
|
|||||||
Write-Host "--- End ---"
|
Write-Host "--- End ---"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Write-PlatformOutput $__v8.Output
|
||||||
|
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-update v1.4 — Update 1C database configuration
|
# db-update v1.13 — 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
|
||||||
@@ -36,36 +36,341 @@ def _find_project_v8path():
|
|||||||
d = parent
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
V8_OWNED_KEYS = [
|
||||||
|
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
|
||||||
|
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
|
||||||
|
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
|
||||||
|
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
|
||||||
|
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||||
|
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||||
|
]
|
||||||
|
IBCMD_OWNED_KEYS = [
|
||||||
|
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||||
|
"--import", "--export", "--apply", "--force", "--create-database",
|
||||||
|
"--user", "--password",
|
||||||
|
]
|
||||||
|
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
|
||||||
|
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
|
||||||
|
|
||||||
|
|
||||||
|
def arg_key_match(token, key):
|
||||||
|
"""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
|
||||||
|
/ClearCache distinct from /C."""
|
||||||
|
if len(token) < len(key):
|
||||||
|
return False
|
||||||
|
if token[: len(key)].lower() != key.lower():
|
||||||
|
return False
|
||||||
|
if len(token) == len(key):
|
||||||
|
return True
|
||||||
|
return not token[len(key)].isalpha()
|
||||||
|
|
||||||
|
|
||||||
|
def project_extra_args(name):
|
||||||
|
"""v8args / ibcmdargs from .v8-project.json — same upward walk as v8path."""
|
||||||
|
d = os.getcwd()
|
||||||
|
while True:
|
||||||
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pf):
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
v = data.get(name)
|
||||||
|
if v:
|
||||||
|
return [str(x) for x in v]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return []
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return []
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def assert_extra_args(extra, engine, hints):
|
||||||
|
"""The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
output key fails with an opaque 1C error — reject what the skill owns itself."""
|
||||||
|
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
|
||||||
|
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
|
||||||
|
for tok in extra:
|
||||||
|
if engine == "ibcmd" and not tok.startswith("-"):
|
||||||
|
print(
|
||||||
|
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||||
|
f"({param} cannot extend the ibcmd command)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
for k in owned:
|
||||||
|
if arg_key_match(tok, k):
|
||||||
|
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||||
|
print(
|
||||||
|
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def format_args_for_display(arglist, engine):
|
||||||
|
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
a leaked password does."""
|
||||||
|
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
|
||||||
|
res = []
|
||||||
|
mask_next = False
|
||||||
|
for tok in arglist:
|
||||||
|
if mask_next:
|
||||||
|
res.append("***")
|
||||||
|
mask_next = False
|
||||||
|
continue
|
||||||
|
hit = None
|
||||||
|
for k in keys:
|
||||||
|
if tok[: len(k)].lower() == k.lower():
|
||||||
|
hit = k
|
||||||
|
break
|
||||||
|
if hit is None:
|
||||||
|
res.append(tok)
|
||||||
|
elif len(tok) == len(hit):
|
||||||
|
res.append(tok)
|
||||||
|
mask_next = True
|
||||||
|
elif tok[len(hit)] == "=":
|
||||||
|
res.append(hit + "=***")
|
||||||
|
else:
|
||||||
|
res.append(hit + "***")
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def extract_extra_args(argv, known_opts):
|
||||||
|
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
|
||||||
|
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
|
||||||
|
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
|
||||||
|
rest, v8, ibcmd = [], [], []
|
||||||
|
i = 0
|
||||||
|
while i < len(argv):
|
||||||
|
low = argv[i].lower()
|
||||||
|
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
|
||||||
|
target = v8 if low == "-additionalv8arguments" else ibcmd
|
||||||
|
i += 1
|
||||||
|
while i < len(argv) and argv[i].lower() not in known_opts:
|
||||||
|
target.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
rest.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
return rest, v8, ibcmd
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
||||||
|
"""Pick the argument list for the selected engine and validate it. An explicitly
|
||||||
|
passed parameter for the other engine is an error; the same keys coming from
|
||||||
|
.v8-project.json simply do not apply — a project may describe both engines.
|
||||||
|
|
||||||
|
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
|
||||||
|
so that form is the documented one and both ports must accept it. A value containing
|
||||||
|
a comma is not supported."""
|
||||||
|
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
|
||||||
|
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
|
||||||
|
if engine == "ibcmd" and v8_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||||
|
"(use -AdditionalIbcmdArguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine != "ibcmd" and ibcmd_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||||
|
"(use -AdditionalV8Arguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine == "ibcmd":
|
||||||
|
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
|
||||||
|
else:
|
||||||
|
extra = project_extra_args("v8args") + list(v8_extra)
|
||||||
|
if extra:
|
||||||
|
assert_extra_args(extra, engine, hints)
|
||||||
|
return extra
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
def _version_key(p):
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
"""Numeric sort key from version dir name."""
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def resolve_v8path(v8path):
|
||||||
"""Resolve path to 1cv8.exe."""
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
if not v8path:
|
if not v8path:
|
||||||
v8path = _find_project_v8path()
|
v8path = _find_project_v8path()
|
||||||
if not v8path:
|
if not v8path:
|
||||||
candidates = (
|
if os.name == "nt":
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
candidates = (
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
)
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
if candidates:
|
if candidates:
|
||||||
v8path = max(candidates, key=_version_key)
|
v8path = max(candidates, key=_version_key)
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
else:
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
if os.path.isdir(v8path):
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def decode_platform_bytes(data):
|
||||||
|
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
|
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — the locale
|
||||||
|
code page (what text=True uses) mangles both."""
|
||||||
|
if not data:
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
return data.decode("utf-8")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
return data.decode("cp866", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def assert_infobase_exists(path):
|
||||||
|
"""These skills work on a ready infobase. Saying so up front beats the platform's
|
||||||
|
"Неверные или отсутствующие параметры соединения" after a launch."""
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
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)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def clean_path(value, param=""):
|
||||||
|
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
|
||||||
|
if not value:
|
||||||
|
return value
|
||||||
|
v = value.strip()
|
||||||
|
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
|
||||||
|
v = v[1:-1].strip()
|
||||||
|
if len(v) > 3 and v[-1] in "\\/":
|
||||||
|
v = v[:-1]
|
||||||
|
if '"' in v:
|
||||||
|
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
def quote_if_needed(token):
|
||||||
|
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
|
||||||
|
verbatim, so a token with a space needs quotes of its own."""
|
||||||
|
if token and (" " in token or "\t" in token) and '"' not in token:
|
||||||
|
return f'"{token}"'
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
def run_v8(v8path, arguments):
|
||||||
|
"""Run 1cv8 in batch mode and capture its console output.
|
||||||
|
|
||||||
|
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
|
||||||
|
escape those quotes, so there the command line is handed over ready-made.
|
||||||
|
"""
|
||||||
|
if os.name == "nt":
|
||||||
|
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||||
|
else:
|
||||||
|
cmd = [v8path] + arguments
|
||||||
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def print_platform_output(result):
|
||||||
|
"""Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
|
||||||
|
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
|
||||||
|
if not text:
|
||||||
|
return
|
||||||
|
limit = 65536
|
||||||
|
if len(text) > limit:
|
||||||
|
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
|
||||||
|
print("--- Вывод платформы ---")
|
||||||
|
print(text)
|
||||||
|
print("--- End ---")
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def describe_exit(code):
|
||||||
|
"""Annotate an abnormal process exit code so a crash isn't reported as a bare number.
|
||||||
|
Batch 1C in a broken/headless environment (no GUI session, no license) can crash mid-run
|
||||||
|
instead of returning a clean error, possibly leaving the infobase locked or half-mutated."""
|
||||||
|
if code is None:
|
||||||
|
return ""
|
||||||
|
win = {
|
||||||
|
3221225477: "0xC0000005 (access violation)", -1073741819: "0xC0000005 (access violation)",
|
||||||
|
3221225781: "0xC0000135 (missing DLL)", -1073741515: "0xC0000135 (missing DLL)",
|
||||||
|
3221226505: "0xC0000409 (stack overrun)", -1073740791: "0xC0000409 (stack overrun)",
|
||||||
|
}
|
||||||
|
if code in win:
|
||||||
|
return f" — abnormal termination, exception {win[code]}; the infobase may be left in an inconsistent state; verify it before retrying"
|
||||||
|
if -64 <= code < 0:
|
||||||
|
try:
|
||||||
|
import signal
|
||||||
|
name = signal.Signals(-code).name
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
name = f"signal {-code}"
|
||||||
|
return (f" — process terminated by {name} (abnormal termination, not a normal exit); "
|
||||||
|
"the infobase may be left in an inconsistent state; verify it before retrying")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _redact(text, *secrets):
|
||||||
|
"""Redact literal secret values (password, user) from a display string —
|
||||||
|
precise, never touches lookalike paths."""
|
||||||
|
for s in secrets:
|
||||||
|
if s:
|
||||||
|
text = text.replace(s, "***")
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
@@ -84,12 +389,34 @@ def main():
|
|||||||
parser.add_argument("-Dynamic", default="", choices=["", "+", "-"])
|
parser.add_argument("-Dynamic", default="", choices=["", "+", "-"])
|
||||||
parser.add_argument("-Server", action="store_true")
|
parser.add_argument("-Server", action="store_true")
|
||||||
parser.add_argument("-WarningsAsErrors", action="store_true")
|
parser.add_argument("-WarningsAsErrors", action="store_true")
|
||||||
args = parser.parse_args()
|
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
|
||||||
|
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
|
||||||
|
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
|
||||||
|
help="Extra ibcmd arguments in --key=value form")
|
||||||
|
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
||||||
|
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
||||||
|
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
||||||
|
assert_infobase_exists(args.InfoBasePath)
|
||||||
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
|
||||||
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
arg_hints = {
|
||||||
|
"/F": "-InfoBasePath",
|
||||||
|
"/S": "-InfoBaseServer + -InfoBaseRef",
|
||||||
|
"/N": "-UserName",
|
||||||
|
"/P": "-Password",
|
||||||
|
"--db-path": "-InfoBasePath",
|
||||||
|
"--user": "-UserName",
|
||||||
|
"--password": "-Password",
|
||||||
|
}
|
||||||
|
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if engine == "ibcmd":
|
if engine == "ibcmd":
|
||||||
if not args.InfoBasePath:
|
if not args.InfoBasePath:
|
||||||
@@ -118,16 +445,13 @@ def main():
|
|||||||
if args.Password:
|
if args.Password:
|
||||||
arguments.append(f"--password={args.Password}")
|
arguments.append(f"--password={args.Password}")
|
||||||
arguments.append(f"--data={ib_data}")
|
arguments.append(f"--data={ib_data}")
|
||||||
print(f"Running: ibcmd {' '.join(arguments)}")
|
arguments.extend(extra_args)
|
||||||
result = subprocess.run([v8path] + arguments, capture_output=True, encoding="utf-8", errors="replace")
|
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||||
|
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||||
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})", file=sys.stderr)
|
print(f"Error updating database configuration (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
|
||||||
if result.stdout:
|
|
||||||
print(result.stdout)
|
|
||||||
if result.stderr:
|
|
||||||
print(result.stderr, file=sys.stderr)
|
|
||||||
sys.exit(result.returncode)
|
sys.exit(result.returncode)
|
||||||
|
|
||||||
# --- Temp dir ---
|
# --- Temp dir ---
|
||||||
@@ -139,14 +463,14 @@ def main():
|
|||||||
arguments = ["DESIGNER"]
|
arguments = ["DESIGNER"]
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
if args.InfoBaseServer and args.InfoBaseRef:
|
||||||
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
|
arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
|
||||||
else:
|
else:
|
||||||
arguments.extend(["/F", args.InfoBasePath])
|
arguments.extend(["/F", f'"{args.InfoBasePath}"'])
|
||||||
|
|
||||||
if args.UserName:
|
if args.UserName:
|
||||||
arguments.append(f"/N{args.UserName}")
|
arguments.append(f'/N"{args.UserName}"')
|
||||||
if args.Password:
|
if args.Password:
|
||||||
arguments.append(f"/P{args.Password}")
|
arguments.append(f'/P"{args.Password}"')
|
||||||
|
|
||||||
arguments.append("/UpdateDBCfg")
|
arguments.append("/UpdateDBCfg")
|
||||||
|
|
||||||
@@ -160,29 +484,26 @@ def main():
|
|||||||
|
|
||||||
# --- Extensions ---
|
# --- Extensions ---
|
||||||
if args.Extension:
|
if args.Extension:
|
||||||
arguments.extend(["-Extension", args.Extension])
|
arguments.extend(["-Extension", f'"{args.Extension}"'])
|
||||||
elif args.AllExtensions:
|
elif args.AllExtensions:
|
||||||
arguments.append("-AllExtensions")
|
arguments.append("-AllExtensions")
|
||||||
|
|
||||||
# --- Output ---
|
# --- Output ---
|
||||||
out_file = os.path.join(temp_dir, "update_log.txt")
|
out_file = os.path.join(temp_dir, "update_log.txt")
|
||||||
arguments.extend(["/Out", out_file])
|
arguments.extend(["/Out", f'"{out_file}"'])
|
||||||
arguments.append("/DisableStartupDialogs")
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
arguments.extend(quote_if_needed(a) for a in extra_args)
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||||
result = subprocess.run(
|
result = run_v8(v8path, arguments)
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
exit_code = result.returncode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
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})", file=sys.stderr)
|
print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
|
||||||
|
|
||||||
if os.path.isfile(out_file):
|
if os.path.isfile(out_file):
|
||||||
try:
|
try:
|
||||||
@@ -195,6 +516,7 @@ def main():
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
print_platform_output(result)
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" <п
|
|||||||
| `-Password <пароль>` | нет | Пароль |
|
| `-Password <пароль>` | нет | Пароль |
|
||||||
| `-SourceFile <путь>` | да | Путь к корневому XML-файлу исходников |
|
| `-SourceFile <путь>` | да | Путь к корневому XML-файлу исходников |
|
||||||
| `-OutputFile <путь>` | да | Путь к выходному EPF/ERF-файлу |
|
| `-OutputFile <путь>` | да | Путь к выходному EPF/ERF-файлу |
|
||||||
|
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
|
||||||
|
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
|
||||||
|
|
||||||
> `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных
|
> `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# epf-build v1.4 — Build external data processor or report (EPF/ERF) from XML sources
|
# epf-build v1.12 — 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 не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Сборка внешней обработки/отчёта 1С из XML-исходников
|
Сборка внешней обработки/отчёта 1С из XML-исходников
|
||||||
@@ -32,6 +33,12 @@
|
|||||||
.PARAMETER OutputFile
|
.PARAMETER OutputFile
|
||||||
Путь к выходному EPF/ERF-файлу
|
Путь к выходному EPF/ERF-файлу
|
||||||
|
|
||||||
|
.PARAMETER AdditionalV8Arguments
|
||||||
|
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
|
||||||
|
|
||||||
|
.PARAMETER AdditionalIbcmdArguments
|
||||||
|
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
|
||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
.\epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src\МояОбработка.xml" -OutputFile "build\МояОбработка.epf"
|
.\epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src\МояОбработка.xml" -OutputFile "build\МояОбработка.epf"
|
||||||
|
|
||||||
@@ -63,12 +70,184 @@ param(
|
|||||||
[string]$SourceFile,
|
[string]$SourceFile,
|
||||||
|
|
||||||
[Parameter(Mandatory=$true)]
|
[Parameter(Mandatory=$true)]
|
||||||
[string]$OutputFile
|
[string]$OutputFile,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalV8Arguments = @(),
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalIbcmdArguments = @()
|
||||||
)
|
)
|
||||||
|
|
||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
function Protect-Secrets {
|
||||||
|
# Redact literal secret values from a display string (String.Replace is literal, not regex).
|
||||||
|
param([string]$Text, [string[]]$Secrets)
|
||||||
|
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
|
||||||
|
return $Text
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
$script:V8OwnedKeys = @(
|
||||||
|
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
|
||||||
|
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
|
||||||
|
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
|
||||||
|
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
|
||||||
|
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||||
|
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||||
|
)
|
||||||
|
$script:IbcmdOwnedKeys = @(
|
||||||
|
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||||
|
'--import', '--export', '--apply', '--force', '--create-database',
|
||||||
|
'--user', '--password'
|
||||||
|
)
|
||||||
|
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
|
||||||
|
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
|
||||||
|
|
||||||
|
function Test-ArgKeyMatch {
|
||||||
|
# A token matches a key when it equals the key, or starts with it and the next
|
||||||
|
# character is not a letter — catches glued /N"user" and --password=x, while
|
||||||
|
# keeping /ClearCache distinct from /C.
|
||||||
|
param([string]$Token, [string]$Key)
|
||||||
|
if ($Token.Length -lt $Key.Length) { return $false }
|
||||||
|
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
|
||||||
|
if ($Token.Length -eq $Key.Length) { return $true }
|
||||||
|
return -not [char]::IsLetter($Token[$Key.Length])
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ProjectExtraArgs {
|
||||||
|
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
|
||||||
|
param([string]$Name)
|
||||||
|
$dir = (Get-Location).Path
|
||||||
|
while ($dir) {
|
||||||
|
$pf = Join-Path $dir ".v8-project.json"
|
||||||
|
if (Test-Path $pf) {
|
||||||
|
try {
|
||||||
|
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
|
||||||
|
} catch {}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
$parent = Split-Path $dir -Parent
|
||||||
|
if (-not $parent -or $parent -eq $dir) { break }
|
||||||
|
$dir = $parent
|
||||||
|
}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Assert-ExtraArgs {
|
||||||
|
# The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
# output key fails with an opaque 1C error — reject what the skill owns itself.
|
||||||
|
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
|
||||||
|
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
|
||||||
|
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
|
||||||
|
foreach ($tok in $ExtraArgs) {
|
||||||
|
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
|
||||||
|
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
foreach ($k in $owned) {
|
||||||
|
if (Test-ArgKeyMatch $tok $k) {
|
||||||
|
$hint = ''
|
||||||
|
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
|
||||||
|
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-ExtraArgs {
|
||||||
|
# Pick the argument list for the selected engine and validate it. An explicitly passed
|
||||||
|
# parameter for the other engine is an error; the same keys coming from .v8-project.json
|
||||||
|
# simply do not apply — a project may describe both engines.
|
||||||
|
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
|
||||||
|
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
|
||||||
|
# space-separated values spill into positional ones, a comma-joined list arrives as a
|
||||||
|
# single token. So accept the repo's list convention (comma-separated) and split here;
|
||||||
|
# a native array call keeps working. A value containing a comma is not supported.
|
||||||
|
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -eq 'ibcmd') {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
|
||||||
|
} else {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
|
||||||
|
}
|
||||||
|
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
|
||||||
|
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
|
||||||
|
# would nest the array — the tokens would then be glued into one argument.
|
||||||
|
return $extra
|
||||||
|
}
|
||||||
|
|
||||||
|
function Format-ArgsForDisplay {
|
||||||
|
# Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
# a leaked password does.
|
||||||
|
param([string[]]$ArgList, [string]$Engine)
|
||||||
|
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
|
||||||
|
$res = @()
|
||||||
|
$maskNext = $false
|
||||||
|
foreach ($tok in $ArgList) {
|
||||||
|
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
|
||||||
|
$hit = $null
|
||||||
|
foreach ($k in $keys) {
|
||||||
|
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
|
||||||
|
}
|
||||||
|
if (-not $hit) { $res += $tok; continue }
|
||||||
|
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
|
||||||
|
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
|
||||||
|
else { $res += ($hit + '***') }
|
||||||
|
}
|
||||||
|
return ,$res
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-CleanPath {
|
||||||
|
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
# inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
|
||||||
|
param([string]$Value, [string]$ParamName)
|
||||||
|
if (-not $Value) { return $Value }
|
||||||
|
$v = $Value.Trim()
|
||||||
|
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
|
||||||
|
$v = $v.Substring(1, $v.Length - 2).Trim()
|
||||||
|
}
|
||||||
|
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
|
||||||
|
if ($v.Contains('"')) {
|
||||||
|
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
return $v
|
||||||
|
}
|
||||||
|
|
||||||
|
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
|
||||||
|
$InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath'
|
||||||
|
$SourceFile = ConvertTo-CleanPath $SourceFile '-SourceFile'
|
||||||
|
$OutputFile = ConvertTo-CleanPath $OutputFile '-OutputFile'
|
||||||
|
|
||||||
|
function Assert-InfoBaseExists {
|
||||||
|
# These skills work on a ready infobase. Saying so up front beats the platform's
|
||||||
|
# "Неверные или отсутствующие параметры соединения" after a launch.
|
||||||
|
param([string]$Path)
|
||||||
|
if (-not $Path) { return }
|
||||||
|
if (-not (Test-Path (Join-Path $Path "1Cv8.1CD"))) {
|
||||||
|
Write-Host "Error: information base not found at $Path (no 1Cv8.1CD)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert-InfoBaseExists $InfoBasePath
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
function Find-ProjectV8Path {
|
||||||
$dir = (Get-Location).Path
|
$dir = (Get-Location).Path
|
||||||
@@ -99,7 +278,7 @@ if (-not $V8Path) {
|
|||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,12 +287,93 @@ if (Test-Path $V8Path -PathType Container) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
|
function ConvertFrom-PlatformBytes {
|
||||||
|
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
|
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
|
||||||
|
# one of them outright mangles Cyrillic.
|
||||||
|
param([byte[]]$Bytes)
|
||||||
|
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
|
||||||
|
try {
|
||||||
|
$strict = New-Object System.Text.UTF8Encoding($false, $true)
|
||||||
|
return $strict.GetString($Bytes)
|
||||||
|
} catch {
|
||||||
|
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-PlatformProcess {
|
||||||
|
# Run the platform non-interactively and capture its console output. A closed stdin pipe
|
||||||
|
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
|
||||||
|
# text out of our stream until we print it labelled (and out of the wrong encoding).
|
||||||
|
# Returns @{ Output; ExitCode }.
|
||||||
|
#
|
||||||
|
# Quoting differs by engine, so the caller says which it built:
|
||||||
|
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
|
||||||
|
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
|
||||||
|
# which is where 1C's own parser expects them; quoting again breaks the value.
|
||||||
|
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = if ($PreQuoted) {
|
||||||
|
$ProcArgs -join ' '
|
||||||
|
} else {
|
||||||
|
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
}
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
# stderr is drained in parallel: reading the streams one after another deadlocks
|
||||||
|
# as soon as the other one fills its pipe buffer.
|
||||||
|
$errMs = New-Object System.IO.MemoryStream
|
||||||
|
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
|
||||||
|
$outMs = New-Object System.IO.MemoryStream
|
||||||
|
$p.StandardOutput.BaseStream.CopyTo($outMs)
|
||||||
|
$errTask.Wait()
|
||||||
|
$p.WaitForExit()
|
||||||
|
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
|
||||||
|
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-PlatformOutput {
|
||||||
|
# Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
|
||||||
|
param([string]$Text)
|
||||||
|
if (-not $Text) { return }
|
||||||
|
$t = $Text.TrimEnd()
|
||||||
|
if (-not $t) { return }
|
||||||
|
$limit = 65536
|
||||||
|
if ($t.Length -gt $limit) {
|
||||||
|
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
|
||||||
|
}
|
||||||
|
Write-Host "--- Вывод платформы ---"
|
||||||
|
Write-Host $t
|
||||||
|
Write-Host "--- End ---"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function Test-OutputNonEmpty {
|
||||||
|
# Postcondition: the platform must have produced a non-empty output file.
|
||||||
|
# Exit code 0 without it (broken/headless env) is a false success — reject it.
|
||||||
|
param([string]$Path)
|
||||||
|
return (Test-Path $Path -PathType Leaf) -and ((Get-Item $Path -ErrorAction SilentlyContinue).Length -gt 0)
|
||||||
|
}
|
||||||
|
|
||||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' }
|
||||||
|
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
|
||||||
if ($engine -eq "ibcmd" -and $InfoBaseServer -and $InfoBaseRef) {
|
if ($engine -eq "ibcmd" -and $InfoBaseServer -and $InfoBaseRef) {
|
||||||
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath or omit for stub)" -ForegroundColor Red
|
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath or omit for stub)" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
@@ -126,8 +386,20 @@ if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
|||||||
$autoBasePath = Join-Path $env:TEMP "epf_stub_db_$(Get-Random)"
|
$autoBasePath = Join-Path $env:TEMP "epf_stub_db_$(Get-Random)"
|
||||||
$stubScript = Join-Path $PSScriptRoot "stub-db-create.ps1"
|
$stubScript = Join-Path $PSScriptRoot "stub-db-create.ps1"
|
||||||
Write-Host "No database specified. Creating temporary stub database..."
|
Write-Host "No database specified. Creating temporary stub database..."
|
||||||
$stubArgs = "-SourceDir `"$sourceDir`" -V8Path `"$V8Path`" -TempBasePath `"$autoBasePath`""
|
# The stub runs its own platform processes (CREATEINFOBASE, LoadConfigFromFiles,
|
||||||
$stubProc = Start-Process -FilePath "powershell.exe" -ArgumentList "-NoProfile -File `"$stubScript`" $stubArgs" -NoNewWindow -Wait -PassThru
|
# UpdateDBCfg) — they need the same extra arguments as the final build. Only the
|
||||||
|
# explicit ones are forwarded: the stub reads .v8-project.json itself.
|
||||||
|
# Invoked via -Command, not -File: -File takes the tail literally, so an array
|
||||||
|
# parameter would arrive as a single comma-glued token.
|
||||||
|
$q = { param($s) "'" + ($s -replace "'", "''") + "'" }
|
||||||
|
$stubCmd = "& $(& $q $stubScript) -SourceDir $(& $q $sourceDir) -V8Path $(& $q $V8Path) -TempBasePath $(& $q $autoBasePath)"
|
||||||
|
if ($AdditionalV8Arguments.Count -gt 0) {
|
||||||
|
$stubCmd += " -AdditionalV8Arguments " + (($AdditionalV8Arguments | ForEach-Object { & $q $_ }) -join ',')
|
||||||
|
}
|
||||||
|
if ($AdditionalIbcmdArguments.Count -gt 0) {
|
||||||
|
$stubCmd += " -AdditionalIbcmdArguments " + (($AdditionalIbcmdArguments | ForEach-Object { & $q $_ }) -join ',')
|
||||||
|
}
|
||||||
|
$stubProc = Start-Process -FilePath "powershell.exe" -ArgumentList "-NoProfile -Command `"$stubCmd`"" -NoNewWindow -Wait -PassThru
|
||||||
if ($stubProc.ExitCode -ne 0) {
|
if ($stubProc.ExitCode -ne 0) {
|
||||||
Write-Host "Error: failed to create stub database" -ForegroundColor Red
|
Write-Host "Error: failed to create stub database" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
@@ -160,15 +432,21 @@ try {
|
|||||||
if ($UserName) { $arguments += "--user=$UserName" }
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
if ($Password) { $arguments += "--password=$Password" }
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
$arguments += "--data=$tempDir"
|
$arguments += "--data=$tempDir"
|
||||||
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
$arguments += $extraArgs
|
||||||
$output = & $V8Path @arguments 2>&1
|
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
||||||
$exitCode = $LASTEXITCODE
|
$__ib = Invoke-PlatformProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
$outMissing = ($exitCode -eq 0) -and -not (Test-OutputNonEmpty $OutputFile)
|
||||||
|
if ($outMissing) { $exitCode = 1 }
|
||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
Write-Host "External data processor/report built successfully: $OutputFile" -ForegroundColor Green
|
Write-Host "External data processor/report built successfully: $OutputFile" -ForegroundColor Green
|
||||||
|
} elseif ($outMissing) {
|
||||||
|
Write-Host "Error: exit code 0 but no non-empty file at $OutputFile — build produced no output" -ForegroundColor Red
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error building external data processor/report (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error building external data processor/report (code: $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
if ($output) { Write-Host ($output | Out-String) }
|
Write-PlatformOutput $output
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,15 +469,21 @@ try {
|
|||||||
$outFile = Join-Path $tempDir "build_log.txt"
|
$outFile = Join-Path $tempDir "build_log.txt"
|
||||||
$arguments += "/Out", "`"$outFile`""
|
$arguments += "/Out", "`"$outFile`""
|
||||||
$arguments += "/DisableStartupDialogs"
|
$arguments += "/DisableStartupDialogs"
|
||||||
|
$arguments += $extraArgs
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
|
||||||
$exitCode = $process.ExitCode
|
$exitCode = $__v8.ExitCode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
|
# Postcondition: exit 0 without a non-empty output file is a false success.
|
||||||
|
$outMissing = ($exitCode -eq 0) -and -not (Test-OutputNonEmpty $OutputFile)
|
||||||
|
if ($outMissing) { $exitCode = 1 }
|
||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
Write-Host "Build completed successfully: $OutputFile" -ForegroundColor Green
|
Write-Host "Build completed successfully: $OutputFile" -ForegroundColor Green
|
||||||
|
} elseif ($outMissing) {
|
||||||
|
Write-Host "Error: exit code 0 but no non-empty file at $OutputFile — build produced no output" -ForegroundColor Red
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error building (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error building (code: $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
@@ -212,6 +496,7 @@ try {
|
|||||||
Write-Host "--- End ---"
|
Write-Host "--- End ---"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Write-PlatformOutput $__v8.Output
|
||||||
|
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# epf-build v1.4 — Build external data processor or report (EPF/ERF) from XML sources
|
# epf-build v1.12 — 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
|
||||||
@@ -36,36 +36,323 @@ def _find_project_v8path():
|
|||||||
d = parent
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
V8_OWNED_KEYS = [
|
||||||
|
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
|
||||||
|
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
|
||||||
|
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
|
||||||
|
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
|
||||||
|
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||||
|
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||||
|
]
|
||||||
|
IBCMD_OWNED_KEYS = [
|
||||||
|
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||||
|
"--import", "--export", "--apply", "--force", "--create-database",
|
||||||
|
"--user", "--password",
|
||||||
|
]
|
||||||
|
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
|
||||||
|
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
|
||||||
|
|
||||||
|
|
||||||
|
def arg_key_match(token, key):
|
||||||
|
"""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
|
||||||
|
/ClearCache distinct from /C."""
|
||||||
|
if len(token) < len(key):
|
||||||
|
return False
|
||||||
|
if token[: len(key)].lower() != key.lower():
|
||||||
|
return False
|
||||||
|
if len(token) == len(key):
|
||||||
|
return True
|
||||||
|
return not token[len(key)].isalpha()
|
||||||
|
|
||||||
|
|
||||||
|
def project_extra_args(name):
|
||||||
|
"""v8args / ibcmdargs from .v8-project.json — same upward walk as v8path."""
|
||||||
|
d = os.getcwd()
|
||||||
|
while True:
|
||||||
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pf):
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
v = data.get(name)
|
||||||
|
if v:
|
||||||
|
return [str(x) for x in v]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return []
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return []
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def assert_extra_args(extra, engine, hints):
|
||||||
|
"""The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
output key fails with an opaque 1C error — reject what the skill owns itself."""
|
||||||
|
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
|
||||||
|
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
|
||||||
|
for tok in extra:
|
||||||
|
if engine == "ibcmd" and not tok.startswith("-"):
|
||||||
|
print(
|
||||||
|
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||||
|
f"({param} cannot extend the ibcmd command)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
for k in owned:
|
||||||
|
if arg_key_match(tok, k):
|
||||||
|
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||||
|
print(
|
||||||
|
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def format_args_for_display(arglist, engine):
|
||||||
|
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
a leaked password does."""
|
||||||
|
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
|
||||||
|
res = []
|
||||||
|
mask_next = False
|
||||||
|
for tok in arglist:
|
||||||
|
if mask_next:
|
||||||
|
res.append("***")
|
||||||
|
mask_next = False
|
||||||
|
continue
|
||||||
|
hit = None
|
||||||
|
for k in keys:
|
||||||
|
if tok[: len(k)].lower() == k.lower():
|
||||||
|
hit = k
|
||||||
|
break
|
||||||
|
if hit is None:
|
||||||
|
res.append(tok)
|
||||||
|
elif len(tok) == len(hit):
|
||||||
|
res.append(tok)
|
||||||
|
mask_next = True
|
||||||
|
elif tok[len(hit)] == "=":
|
||||||
|
res.append(hit + "=***")
|
||||||
|
else:
|
||||||
|
res.append(hit + "***")
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def extract_extra_args(argv, known_opts):
|
||||||
|
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
|
||||||
|
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
|
||||||
|
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
|
||||||
|
rest, v8, ibcmd = [], [], []
|
||||||
|
i = 0
|
||||||
|
while i < len(argv):
|
||||||
|
low = argv[i].lower()
|
||||||
|
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
|
||||||
|
target = v8 if low == "-additionalv8arguments" else ibcmd
|
||||||
|
i += 1
|
||||||
|
while i < len(argv) and argv[i].lower() not in known_opts:
|
||||||
|
target.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
rest.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
return rest, v8, ibcmd
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
||||||
|
"""Pick the argument list for the selected engine and validate it. An explicitly
|
||||||
|
passed parameter for the other engine is an error; the same keys coming from
|
||||||
|
.v8-project.json simply do not apply — a project may describe both engines.
|
||||||
|
|
||||||
|
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
|
||||||
|
so that form is the documented one and both ports must accept it. A value containing
|
||||||
|
a comma is not supported."""
|
||||||
|
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
|
||||||
|
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
|
||||||
|
if engine == "ibcmd" and v8_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||||
|
"(use -AdditionalIbcmdArguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine != "ibcmd" and ibcmd_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||||
|
"(use -AdditionalV8Arguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine == "ibcmd":
|
||||||
|
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
|
||||||
|
else:
|
||||||
|
extra = project_extra_args("v8args") + list(v8_extra)
|
||||||
|
if extra:
|
||||||
|
assert_extra_args(extra, engine, hints)
|
||||||
|
return extra
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
def _version_key(p):
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
"""Numeric sort key from version dir name."""
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def resolve_v8path(v8path):
|
||||||
"""Resolve path to 1cv8.exe."""
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
if not v8path:
|
if not v8path:
|
||||||
v8path = _find_project_v8path()
|
v8path = _find_project_v8path()
|
||||||
if not v8path:
|
if not v8path:
|
||||||
candidates = (
|
if os.name == "nt":
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
candidates = (
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
)
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
if candidates:
|
if candidates:
|
||||||
v8path = max(candidates, key=_version_key)
|
v8path = max(candidates, key=_version_key)
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
else:
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
if os.path.isdir(v8path):
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def decode_platform_bytes(data):
|
||||||
|
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
|
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — the locale
|
||||||
|
code page (what text=True uses) mangles both."""
|
||||||
|
if not data:
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
return data.decode("utf-8")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
return data.decode("cp866", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def assert_infobase_exists(path):
|
||||||
|
"""These skills work on a ready infobase. Saying so up front beats the platform's
|
||||||
|
"Неверные или отсутствующие параметры соединения" after a launch."""
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
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)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def clean_path(value, param=""):
|
||||||
|
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
|
||||||
|
if not value:
|
||||||
|
return value
|
||||||
|
v = value.strip()
|
||||||
|
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
|
||||||
|
v = v[1:-1].strip()
|
||||||
|
if len(v) > 3 and v[-1] in "\\/":
|
||||||
|
v = v[:-1]
|
||||||
|
if '"' in v:
|
||||||
|
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
def quote_if_needed(token):
|
||||||
|
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
|
||||||
|
verbatim, so a token with a space needs quotes of its own."""
|
||||||
|
if token and (" " in token or "\t" in token) and '"' not in token:
|
||||||
|
return f'"{token}"'
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
def run_v8(v8path, arguments):
|
||||||
|
"""Run 1cv8 in batch mode and capture its console output.
|
||||||
|
|
||||||
|
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
|
||||||
|
escape those quotes, so there the command line is handed over ready-made.
|
||||||
|
"""
|
||||||
|
if os.name == "nt":
|
||||||
|
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||||
|
else:
|
||||||
|
cmd = [v8path] + arguments
|
||||||
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def print_platform_output(result):
|
||||||
|
"""Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
|
||||||
|
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
|
||||||
|
if not text:
|
||||||
|
return
|
||||||
|
limit = 65536
|
||||||
|
if len(text) > limit:
|
||||||
|
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
|
||||||
|
print("--- Вывод платформы ---")
|
||||||
|
print(text)
|
||||||
|
print("--- End ---")
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def output_nonempty(path):
|
||||||
|
"""Postcondition: the platform must have produced a non-empty output file.
|
||||||
|
Exit code 0 without it (broken/headless env) is a false success — reject it."""
|
||||||
|
return os.path.isfile(path) and os.path.getsize(path) > 0
|
||||||
|
|
||||||
|
|
||||||
|
def _redact(text, *secrets):
|
||||||
|
"""Redact literal secret values (password, user) from a display string —
|
||||||
|
precise, never touches lookalike paths."""
|
||||||
|
for s in secrets:
|
||||||
|
if s:
|
||||||
|
text = text.replace(s, "***")
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
@@ -81,11 +368,35 @@ def main():
|
|||||||
parser.add_argument("-Password", default="", help="1C user password")
|
parser.add_argument("-Password", default="", help="1C user password")
|
||||||
parser.add_argument("-SourceFile", required=True, help="Path to root XML source file")
|
parser.add_argument("-SourceFile", required=True, help="Path to root XML source file")
|
||||||
parser.add_argument("-OutputFile", required=True, help="Path to output EPF/ERF file")
|
parser.add_argument("-OutputFile", required=True, help="Path to output EPF/ERF file")
|
||||||
args = parser.parse_args()
|
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
|
||||||
|
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
|
||||||
|
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
|
||||||
|
help="Extra ibcmd arguments in --key=value form")
|
||||||
|
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
||||||
|
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
||||||
|
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
||||||
|
assert_infobase_exists(args.InfoBasePath)
|
||||||
|
args.SourceFile = clean_path(args.SourceFile, "-SourceFile")
|
||||||
|
args.OutputFile = clean_path(args.OutputFile, "-OutputFile")
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
arg_hints = {
|
||||||
|
"/F": "-InfoBasePath",
|
||||||
|
"/S": "-InfoBaseServer + -InfoBaseRef",
|
||||||
|
"/N": "-UserName",
|
||||||
|
"/P": "-Password",
|
||||||
|
"--db-path": "-InfoBasePath",
|
||||||
|
"--user": "-UserName",
|
||||||
|
"--password": "-Password",
|
||||||
|
}
|
||||||
|
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)", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@@ -97,10 +408,16 @@ def main():
|
|||||||
auto_base_path = os.path.join(tempfile.gettempdir(), f"epf_stub_db_{random.randint(0, 999999)}")
|
auto_base_path = os.path.join(tempfile.gettempdir(), f"epf_stub_db_{random.randint(0, 999999)}")
|
||||||
stub_script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "stub-db-create.py")
|
stub_script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "stub-db-create.py")
|
||||||
print("No database specified. Creating temporary stub database...")
|
print("No database specified. Creating temporary stub database...")
|
||||||
result = subprocess.run(
|
stub_cmd = [sys.executable, stub_script, "-SourceDir", source_dir, "-V8Path", v8path,
|
||||||
[sys.executable, stub_script, "-SourceDir", source_dir, "-V8Path", v8path, "-TempBasePath", auto_base_path],
|
"-TempBasePath", auto_base_path]
|
||||||
capture_output=False,
|
# The stub runs its own platform processes (CREATEINFOBASE, LoadConfigFromFiles,
|
||||||
)
|
# UpdateDBCfg) — they need the same extra arguments as the final build. Only the
|
||||||
|
# explicit ones are forwarded: the stub reads .v8-project.json itself.
|
||||||
|
if v8_extra:
|
||||||
|
stub_cmd += ["-AdditionalV8Arguments"] + list(v8_extra)
|
||||||
|
if ibcmd_extra:
|
||||||
|
stub_cmd += ["-AdditionalIbcmdArguments"] + list(ibcmd_extra)
|
||||||
|
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", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@@ -133,50 +450,56 @@ def main():
|
|||||||
if args.Password:
|
if args.Password:
|
||||||
arguments.append(f"--password={args.Password}")
|
arguments.append(f"--password={args.Password}")
|
||||||
arguments.append(f"--data={ib_data}")
|
arguments.append(f"--data={ib_data}")
|
||||||
print(f"Running: ibcmd {' '.join(arguments)}")
|
arguments.extend(extra_args)
|
||||||
result = subprocess.run([v8path] + arguments, capture_output=True, encoding="utf-8", errors="replace")
|
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||||
if result.returncode == 0:
|
result = run_ibcmd([v8path] + arguments, warn_no_user=False)
|
||||||
|
exit_code = result.returncode
|
||||||
|
out_missing = exit_code == 0 and not output_nonempty(args.OutputFile)
|
||||||
|
if out_missing:
|
||||||
|
exit_code = 1
|
||||||
|
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:
|
||||||
|
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — build produced no output", file=sys.stderr)
|
||||||
else:
|
else:
|
||||||
print(f"Error building external data processor/report (code: {result.returncode})", file=sys.stderr)
|
print(f"Error building external data processor/report (code: {exit_code})", file=sys.stderr)
|
||||||
if result.stdout:
|
sys.exit(exit_code)
|
||||||
print(result.stdout)
|
|
||||||
if result.stderr:
|
|
||||||
print(result.stderr, file=sys.stderr)
|
|
||||||
sys.exit(result.returncode)
|
|
||||||
|
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
arguments = ["DESIGNER"]
|
arguments = ["DESIGNER"]
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
if args.InfoBaseServer and args.InfoBaseRef:
|
||||||
arguments += ["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"]
|
arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
|
||||||
else:
|
else:
|
||||||
arguments += ["/F", args.InfoBasePath]
|
arguments += ["/F", f'"{args.InfoBasePath}"']
|
||||||
|
|
||||||
if args.UserName:
|
if args.UserName:
|
||||||
arguments.append(f"/N{args.UserName}")
|
arguments.append(f'/N"{args.UserName}"')
|
||||||
if args.Password:
|
if args.Password:
|
||||||
arguments.append(f"/P{args.Password}")
|
arguments.append(f'/P"{args.Password}"')
|
||||||
|
|
||||||
arguments += ["/LoadExternalDataProcessorOrReportFromFiles", args.SourceFile, args.OutputFile]
|
arguments += ["/LoadExternalDataProcessorOrReportFromFiles", f'"{args.SourceFile}"', f'"{args.OutputFile}"']
|
||||||
|
|
||||||
# --- Output ---
|
# --- Output ---
|
||||||
out_file = os.path.join(temp_dir, "build_log.txt")
|
out_file = os.path.join(temp_dir, "build_log.txt")
|
||||||
arguments += ["/Out", out_file]
|
arguments += ["/Out", f'"{out_file}"']
|
||||||
arguments.append("/DisableStartupDialogs")
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
arguments.extend(quote_if_needed(a) for a in extra_args)
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||||
result = subprocess.run(
|
result = run_v8(v8path, arguments)
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
exit_code = result.returncode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
|
# Postcondition: exit 0 without a non-empty output file is a false success.
|
||||||
|
out_missing = exit_code == 0 and not output_nonempty(args.OutputFile)
|
||||||
|
if out_missing:
|
||||||
|
exit_code = 1
|
||||||
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:
|
||||||
|
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — build produced no output", file=sys.stderr)
|
||||||
else:
|
else:
|
||||||
print(f"Error building (code: {exit_code})", file=sys.stderr)
|
print(f"Error building (code: {exit_code})", file=sys.stderr)
|
||||||
|
|
||||||
@@ -191,6 +514,7 @@ def main():
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
print_platform_output(result)
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# stub-db-create v1.2 — Create temp 1C infobase with metadata stubs for EPF/ERF build
|
# stub-db-create v1.7 — Create temp 1C infobase with metadata stubs for EPF/ERF build
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -7,12 +7,162 @@ param(
|
|||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
[string]$V8Path,
|
[string]$V8Path,
|
||||||
|
|
||||||
[string]$TempBasePath
|
[string]$TempBasePath,
|
||||||
|
|
||||||
|
[string[]]$AdditionalV8Arguments = @(),
|
||||||
|
|
||||||
|
[string[]]$AdditionalIbcmdArguments = @()
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
function ConvertTo-CleanPath {
|
||||||
|
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
# inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
|
||||||
|
param([string]$Value, [string]$ParamName)
|
||||||
|
if (-not $Value) { return $Value }
|
||||||
|
$v = $Value.Trim()
|
||||||
|
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
|
||||||
|
$v = $v.Substring(1, $v.Length - 2).Trim()
|
||||||
|
}
|
||||||
|
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
|
||||||
|
if ($v.Contains('"')) {
|
||||||
|
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
return $v
|
||||||
|
}
|
||||||
|
|
||||||
|
$SourceDir = ConvertTo-CleanPath $SourceDir '-SourceDir'
|
||||||
|
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
|
||||||
|
$TempBasePath = ConvertTo-CleanPath $TempBasePath '-TempBasePath'
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
$script:V8OwnedKeys = @(
|
||||||
|
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
|
||||||
|
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
|
||||||
|
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
|
||||||
|
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
|
||||||
|
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||||
|
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||||
|
)
|
||||||
|
$script:IbcmdOwnedKeys = @(
|
||||||
|
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||||
|
'--import', '--export', '--apply', '--force', '--create-database',
|
||||||
|
'--user', '--password'
|
||||||
|
)
|
||||||
|
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
|
||||||
|
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
|
||||||
|
|
||||||
|
function Test-ArgKeyMatch {
|
||||||
|
# A token matches a key when it equals the key, or starts with it and the next
|
||||||
|
# character is not a letter — catches glued /N"user" and --password=x, while
|
||||||
|
# keeping /ClearCache distinct from /C.
|
||||||
|
param([string]$Token, [string]$Key)
|
||||||
|
if ($Token.Length -lt $Key.Length) { return $false }
|
||||||
|
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
|
||||||
|
if ($Token.Length -eq $Key.Length) { return $true }
|
||||||
|
return -not [char]::IsLetter($Token[$Key.Length])
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ProjectExtraArgs {
|
||||||
|
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
|
||||||
|
param([string]$Name)
|
||||||
|
$dir = (Get-Location).Path
|
||||||
|
while ($dir) {
|
||||||
|
$pf = Join-Path $dir ".v8-project.json"
|
||||||
|
if (Test-Path $pf) {
|
||||||
|
try {
|
||||||
|
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
|
||||||
|
} catch {}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
$parent = Split-Path $dir -Parent
|
||||||
|
if (-not $parent -or $parent -eq $dir) { break }
|
||||||
|
$dir = $parent
|
||||||
|
}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Assert-ExtraArgs {
|
||||||
|
# The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
# output key fails with an opaque 1C error — reject what the skill owns itself.
|
||||||
|
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
|
||||||
|
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
|
||||||
|
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
|
||||||
|
foreach ($tok in $ExtraArgs) {
|
||||||
|
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
|
||||||
|
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
foreach ($k in $owned) {
|
||||||
|
if (Test-ArgKeyMatch $tok $k) {
|
||||||
|
$hint = ''
|
||||||
|
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
|
||||||
|
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-ExtraArgs {
|
||||||
|
# Pick the argument list for the selected engine and validate it. An explicitly passed
|
||||||
|
# parameter for the other engine is an error; the same keys coming from .v8-project.json
|
||||||
|
# simply do not apply — a project may describe both engines.
|
||||||
|
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
|
||||||
|
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
|
||||||
|
# space-separated values spill into positional ones, a comma-joined list arrives as a
|
||||||
|
# single token. So accept the repo's list convention (comma-separated) and split here;
|
||||||
|
# a native array call keeps working. A value containing a comma is not supported.
|
||||||
|
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -eq 'ibcmd') {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
|
||||||
|
} else {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
|
||||||
|
}
|
||||||
|
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
|
||||||
|
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
|
||||||
|
# would nest the array — the tokens would then be glued into one argument.
|
||||||
|
return $extra
|
||||||
|
}
|
||||||
|
|
||||||
|
function Format-ArgsForDisplay {
|
||||||
|
# Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
# a leaked password does.
|
||||||
|
param([string[]]$ArgList, [string]$Engine)
|
||||||
|
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
|
||||||
|
$res = @()
|
||||||
|
$maskNext = $false
|
||||||
|
foreach ($tok in $ArgList) {
|
||||||
|
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
|
||||||
|
$hit = $null
|
||||||
|
foreach ($k in $keys) {
|
||||||
|
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
|
||||||
|
}
|
||||||
|
if (-not $hit) { $res += $tok; continue }
|
||||||
|
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
|
||||||
|
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
|
||||||
|
else { $res += ($hit + '***') }
|
||||||
|
}
|
||||||
|
return ,$res
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# --- 1. Scan XML files for reference types ---
|
# --- 1. Scan XML files for reference types ---
|
||||||
|
|
||||||
$typeMap = @{} # MetadataType -> @(Name1, Name2, ...)
|
$typeMap = @{} # MetadataType -> @(Name1, Name2, ...)
|
||||||
@@ -1253,7 +1403,89 @@ $propsXml </Properties>$childObjLine
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- 5a. Stub via ibcmd (one call: create [--import --apply]) ---
|
# --- 5a. Stub via ibcmd (one call: create [--import --apply]) ---
|
||||||
|
function ConvertFrom-PlatformBytes {
|
||||||
|
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
|
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
|
||||||
|
# one of them outright mangles Cyrillic.
|
||||||
|
param([byte[]]$Bytes)
|
||||||
|
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
|
||||||
|
try {
|
||||||
|
$strict = New-Object System.Text.UTF8Encoding($false, $true)
|
||||||
|
return $strict.GetString($Bytes)
|
||||||
|
} catch {
|
||||||
|
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-PlatformProcess {
|
||||||
|
# Run the platform non-interactively and capture its console output. A closed stdin pipe
|
||||||
|
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
|
||||||
|
# text out of our stream until we print it labelled (and out of the wrong encoding).
|
||||||
|
# Returns @{ Output; ExitCode }.
|
||||||
|
#
|
||||||
|
# Quoting differs by engine, so the caller says which it built:
|
||||||
|
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
|
||||||
|
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
|
||||||
|
# which is where 1C's own parser expects them; quoting again breaks the value.
|
||||||
|
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = if ($PreQuoted) {
|
||||||
|
$ProcArgs -join ' '
|
||||||
|
} else {
|
||||||
|
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
}
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
# stderr is drained in parallel: reading the streams one after another deadlocks
|
||||||
|
# as soon as the other one fills its pipe buffer.
|
||||||
|
$errMs = New-Object System.IO.MemoryStream
|
||||||
|
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
|
||||||
|
$outMs = New-Object System.IO.MemoryStream
|
||||||
|
$p.StandardOutput.BaseStream.CopyTo($outMs)
|
||||||
|
$errTask.Wait()
|
||||||
|
$p.WaitForExit()
|
||||||
|
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
|
||||||
|
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-PlatformOutput {
|
||||||
|
# Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
|
||||||
|
param([string]$Text)
|
||||||
|
if (-not $Text) { return }
|
||||||
|
$t = $Text.TrimEnd()
|
||||||
|
if (-not $t) { return }
|
||||||
|
$limit = 65536
|
||||||
|
if ($t.Length -gt $limit) {
|
||||||
|
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
|
||||||
|
}
|
||||||
|
Write-Host "--- Вывод платформы ---"
|
||||||
|
Write-Host $t
|
||||||
|
Write-Host "--- End ---"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
$stubEngine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
$stubEngine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
$argHints = @{ '/F' = '-TempBasePath'; '--db-path' = '-TempBasePath' }
|
||||||
|
$extraArgs = @(Resolve-ExtraArgs $stubEngine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
|
||||||
|
|
||||||
|
function Format-ArgToken {
|
||||||
|
# Start-Process takes these argument lists as one string, so quote each token that needs it.
|
||||||
|
param([string]$Token)
|
||||||
|
if ($Token -match '[\s"]') { return ' "' + ($Token -replace '"', '\"') + '"' }
|
||||||
|
return " $Token"
|
||||||
|
}
|
||||||
|
$extraArgString = -join ($extraArgs | ForEach-Object { Format-ArgToken $_ })
|
||||||
if ($stubEngine -eq "ibcmd") {
|
if ($stubEngine -eq "ibcmd") {
|
||||||
Write-Host "Creating infobase (ibcmd): $TempBasePath"
|
Write-Host "Creating infobase (ibcmd): $TempBasePath"
|
||||||
$ibData = Join-Path $env:TEMP "stub_data_$(Get-Random)"
|
$ibData = Join-Path $env:TEMP "stub_data_$(Get-Random)"
|
||||||
@@ -1261,11 +1493,13 @@ if ($stubEngine -eq "ibcmd") {
|
|||||||
$ibArgs = @("infobase", "create", "--db-path=$TempBasePath", "--create-database")
|
$ibArgs = @("infobase", "create", "--db-path=$TempBasePath", "--create-database")
|
||||||
if ($hasRefTypes) { $ibArgs += "--import=$(Join-Path $TempBasePath 'cfg')", "--apply", "--force" }
|
if ($hasRefTypes) { $ibArgs += "--import=$(Join-Path $TempBasePath 'cfg')", "--apply", "--force" }
|
||||||
$ibArgs += "--data=$ibData"
|
$ibArgs += "--data=$ibData"
|
||||||
$ibOut = & $V8Path @ibArgs 2>&1
|
$ibArgs += $extraArgs
|
||||||
$ibRc = $LASTEXITCODE
|
$__ib = Invoke-PlatformProcess $V8Path $ibArgs
|
||||||
|
$ibOut = $__ib.Output
|
||||||
|
$ibRc = $__ib.ExitCode
|
||||||
Remove-Item -Path $ibData -Recurse -Force -ErrorAction SilentlyContinue
|
Remove-Item -Path $ibData -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
if ($ibRc -ne 0) {
|
if ($ibRc -ne 0) {
|
||||||
if ($ibOut) { Write-Host ($ibOut | Out-String) }
|
Write-PlatformOutput $ibOut
|
||||||
Write-Error "Failed to create stub infobase (code: $ibRc)"
|
Write-Error "Failed to create stub infobase (code: $ibRc)"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -1277,9 +1511,10 @@ if ($stubEngine -eq "ibcmd") {
|
|||||||
|
|
||||||
# --- 5. Create infobase ---
|
# --- 5. Create infobase ---
|
||||||
Write-Host "Creating infobase: $TempBasePath"
|
Write-Host "Creating infobase: $TempBasePath"
|
||||||
$createArgs = "CREATEINFOBASE File=`"$TempBasePath`" /DisableStartupDialogs"
|
$createArgs = "CREATEINFOBASE File=`"$TempBasePath`" /DisableStartupDialogs" + $extraArgString
|
||||||
$proc = Start-Process -FilePath $V8Path -ArgumentList $createArgs -NoNewWindow -Wait -PassThru
|
$proc = Invoke-PlatformProcess $V8Path @($createArgs) -PreQuoted
|
||||||
if ($proc.ExitCode -ne 0) {
|
if ($proc.ExitCode -ne 0) {
|
||||||
|
Write-PlatformOutput $proc.Output
|
||||||
Write-Error "Failed to create infobase (code: $($proc.ExitCode))"
|
Write-Error "Failed to create infobase (code: $($proc.ExitCode))"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -1290,10 +1525,11 @@ if ($hasRefTypes) {
|
|||||||
# LoadConfigFromFiles
|
# LoadConfigFromFiles
|
||||||
Write-Host "Loading configuration from files..."
|
Write-Host "Loading configuration from files..."
|
||||||
$loadLog = Join-Path $env:TEMP "stub_load_log.txt"
|
$loadLog = Join-Path $env:TEMP "stub_load_log.txt"
|
||||||
$loadArgs = "DESIGNER /F`"$TempBasePath`" /LoadConfigFromFiles `"$cfgDir`" /Out `"$loadLog`" /DisableStartupDialogs"
|
$loadArgs = "DESIGNER /F`"$TempBasePath`" /LoadConfigFromFiles `"$cfgDir`" /Out `"$loadLog`" /DisableStartupDialogs" + $extraArgString
|
||||||
$proc = Start-Process -FilePath $V8Path -ArgumentList $loadArgs -NoNewWindow -Wait -PassThru
|
$proc = Invoke-PlatformProcess $V8Path @($loadArgs) -PreQuoted
|
||||||
if ($proc.ExitCode -ne 0) {
|
if ($proc.ExitCode -ne 0) {
|
||||||
if (Test-Path $loadLog) { Get-Content $loadLog -Raw -ErrorAction SilentlyContinue | Write-Host }
|
if (Test-Path $loadLog) { Get-Content $loadLog -Raw -ErrorAction SilentlyContinue | Write-Host }
|
||||||
|
Write-PlatformOutput $proc.Output
|
||||||
Write-Error "Failed to load config (code: $($proc.ExitCode))"
|
Write-Error "Failed to load config (code: $($proc.ExitCode))"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -1301,10 +1537,11 @@ if ($hasRefTypes) {
|
|||||||
# UpdateDBCfg
|
# UpdateDBCfg
|
||||||
Write-Host "Updating database configuration..."
|
Write-Host "Updating database configuration..."
|
||||||
$updateLog = Join-Path $env:TEMP "stub_update_log.txt"
|
$updateLog = Join-Path $env:TEMP "stub_update_log.txt"
|
||||||
$updateArgs = "DESIGNER /F`"$TempBasePath`" /UpdateDBCfg /Out `"$updateLog`" /DisableStartupDialogs"
|
$updateArgs = "DESIGNER /F`"$TempBasePath`" /UpdateDBCfg /Out `"$updateLog`" /DisableStartupDialogs" + $extraArgString
|
||||||
$proc = Start-Process -FilePath $V8Path -ArgumentList $updateArgs -NoNewWindow -Wait -PassThru
|
$proc = Invoke-PlatformProcess $V8Path @($updateArgs) -PreQuoted
|
||||||
if ($proc.ExitCode -ne 0) {
|
if ($proc.ExitCode -ne 0) {
|
||||||
if (Test-Path $updateLog) { Get-Content $updateLog -Raw -ErrorAction SilentlyContinue | Write-Host }
|
if (Test-Path $updateLog) { Get-Content $updateLog -Raw -ErrorAction SilentlyContinue | Write-Host }
|
||||||
|
Write-PlatformOutput $proc.Output
|
||||||
Write-Error "Failed to update DB config (code: $($proc.ExitCode))"
|
Write-Error "Failed to update DB config (code: $($proc.ExitCode))"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# stub-db-create v1.2 — Create temp 1C infobase with metadata stubs for EPF/ERF build
|
# stub-db-create v1.7 — Create temp 1C infobase with metadata stubs for EPF/ERF build
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -12,6 +12,256 @@ import tempfile
|
|||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def decode_platform_bytes(data):
|
||||||
|
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
|
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — the locale
|
||||||
|
code page (what text=True uses) mangles both."""
|
||||||
|
if not data:
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
return data.decode("utf-8")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
return data.decode("cp866", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def clean_path(value, param=""):
|
||||||
|
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
|
||||||
|
if not value:
|
||||||
|
return value
|
||||||
|
v = value.strip()
|
||||||
|
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
|
||||||
|
v = v[1:-1].strip()
|
||||||
|
if len(v) > 3 and v[-1] in "\\/":
|
||||||
|
v = v[:-1]
|
||||||
|
if '"' in v:
|
||||||
|
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
def quote_if_needed(token):
|
||||||
|
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
|
||||||
|
verbatim, so a token with a space needs quotes of its own."""
|
||||||
|
if token and (" " in token or "\t" in token) and '"' not in token:
|
||||||
|
return f'"{token}"'
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
def run_v8(v8path, arguments):
|
||||||
|
"""Run 1cv8 in batch mode and capture its console output.
|
||||||
|
|
||||||
|
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
|
||||||
|
escape those quotes, so there the command line is handed over ready-made.
|
||||||
|
"""
|
||||||
|
if os.name == "nt":
|
||||||
|
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||||
|
else:
|
||||||
|
cmd = [v8path] + arguments
|
||||||
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def print_platform_output(result):
|
||||||
|
"""Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
|
||||||
|
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
|
||||||
|
if not text:
|
||||||
|
return
|
||||||
|
limit = 65536
|
||||||
|
if len(text) > limit:
|
||||||
|
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
|
||||||
|
print("--- Вывод платформы ---")
|
||||||
|
print(text)
|
||||||
|
print("--- End ---")
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
V8_OWNED_KEYS = [
|
||||||
|
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
|
||||||
|
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
|
||||||
|
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
|
||||||
|
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
|
||||||
|
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||||
|
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||||
|
]
|
||||||
|
IBCMD_OWNED_KEYS = [
|
||||||
|
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||||
|
"--import", "--export", "--apply", "--force", "--create-database",
|
||||||
|
"--user", "--password",
|
||||||
|
]
|
||||||
|
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
|
||||||
|
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
|
||||||
|
|
||||||
|
|
||||||
|
def arg_key_match(token, key):
|
||||||
|
"""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
|
||||||
|
/ClearCache distinct from /C."""
|
||||||
|
if len(token) < len(key):
|
||||||
|
return False
|
||||||
|
if token[: len(key)].lower() != key.lower():
|
||||||
|
return False
|
||||||
|
if len(token) == len(key):
|
||||||
|
return True
|
||||||
|
return not token[len(key)].isalpha()
|
||||||
|
|
||||||
|
|
||||||
|
def project_extra_args(name):
|
||||||
|
"""v8args / ibcmdargs from .v8-project.json — same upward walk as v8path."""
|
||||||
|
d = os.getcwd()
|
||||||
|
while True:
|
||||||
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pf):
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
v = data.get(name)
|
||||||
|
if v:
|
||||||
|
return [str(x) for x in v]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return []
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return []
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def assert_extra_args(extra, engine, hints):
|
||||||
|
"""The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
output key fails with an opaque 1C error — reject what the skill owns itself."""
|
||||||
|
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
|
||||||
|
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
|
||||||
|
for tok in extra:
|
||||||
|
if engine == "ibcmd" and not tok.startswith("-"):
|
||||||
|
print(
|
||||||
|
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||||
|
f"({param} cannot extend the ibcmd command)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
for k in owned:
|
||||||
|
if arg_key_match(tok, k):
|
||||||
|
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||||
|
print(
|
||||||
|
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def format_args_for_display(arglist, engine):
|
||||||
|
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
a leaked password does."""
|
||||||
|
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
|
||||||
|
res = []
|
||||||
|
mask_next = False
|
||||||
|
for tok in arglist:
|
||||||
|
if mask_next:
|
||||||
|
res.append("***")
|
||||||
|
mask_next = False
|
||||||
|
continue
|
||||||
|
hit = None
|
||||||
|
for k in keys:
|
||||||
|
if tok[: len(k)].lower() == k.lower():
|
||||||
|
hit = k
|
||||||
|
break
|
||||||
|
if hit is None:
|
||||||
|
res.append(tok)
|
||||||
|
elif len(tok) == len(hit):
|
||||||
|
res.append(tok)
|
||||||
|
mask_next = True
|
||||||
|
elif tok[len(hit)] == "=":
|
||||||
|
res.append(hit + "=***")
|
||||||
|
else:
|
||||||
|
res.append(hit + "***")
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def extract_extra_args(argv, known_opts):
|
||||||
|
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
|
||||||
|
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
|
||||||
|
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
|
||||||
|
rest, v8, ibcmd = [], [], []
|
||||||
|
i = 0
|
||||||
|
while i < len(argv):
|
||||||
|
low = argv[i].lower()
|
||||||
|
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
|
||||||
|
target = v8 if low == "-additionalv8arguments" else ibcmd
|
||||||
|
i += 1
|
||||||
|
while i < len(argv) and argv[i].lower() not in known_opts:
|
||||||
|
target.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
rest.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
return rest, v8, ibcmd
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
||||||
|
"""Pick the argument list for the selected engine and validate it. An explicitly
|
||||||
|
passed parameter for the other engine is an error; the same keys coming from
|
||||||
|
.v8-project.json simply do not apply — a project may describe both engines.
|
||||||
|
|
||||||
|
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
|
||||||
|
so that form is the documented one and both ports must accept it. A value containing
|
||||||
|
a comma is not supported."""
|
||||||
|
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
|
||||||
|
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
|
||||||
|
if engine == "ibcmd" and v8_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||||
|
"(use -AdditionalIbcmdArguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine != "ibcmd" and ibcmd_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||||
|
"(use -AdditionalV8Arguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine == "ibcmd":
|
||||||
|
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
|
||||||
|
else:
|
||||||
|
extra = project_extra_args("v8args") + list(v8_extra)
|
||||||
|
if extra:
|
||||||
|
assert_extra_args(extra, engine, hints)
|
||||||
|
return extra
|
||||||
|
|
||||||
|
|
||||||
def new_uuid():
|
def new_uuid():
|
||||||
return str(uuid.uuid4())
|
return str(uuid.uuid4())
|
||||||
|
|
||||||
@@ -781,7 +1031,17 @@ def main():
|
|||||||
parser.add_argument('-SourceDir', required=True)
|
parser.add_argument('-SourceDir', required=True)
|
||||||
parser.add_argument('-V8Path', required=True)
|
parser.add_argument('-V8Path', required=True)
|
||||||
parser.add_argument('-TempBasePath', default='')
|
parser.add_argument('-TempBasePath', default='')
|
||||||
args = parser.parse_args()
|
parser.add_argument('-AdditionalV8Arguments', nargs='*', default=[],
|
||||||
|
help='Extra 1cv8 arguments, e.g. /UseHwLicenses+')
|
||||||
|
parser.add_argument('-AdditionalIbcmdArguments', nargs='*', default=[],
|
||||||
|
help='Extra ibcmd arguments in --key=value form')
|
||||||
|
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
||||||
|
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
args.SourceDir = clean_path(args.SourceDir, "-SourceDir")
|
||||||
|
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
||||||
|
args.TempBasePath = clean_path(args.TempBasePath, "-TempBasePath")
|
||||||
|
|
||||||
type_map = scan_ref_types(args.SourceDir)
|
type_map = scan_ref_types(args.SourceDir)
|
||||||
register_columns = scan_register_columns(args.SourceDir)
|
register_columns = scan_register_columns(args.SourceDir)
|
||||||
@@ -1036,6 +1296,10 @@ def main():
|
|||||||
|
|
||||||
# Stub via ibcmd (one call: create [--import --apply])
|
# Stub via ibcmd (one call: create [--import --apply])
|
||||||
stub_engine = "ibcmd" if os.path.basename(args.V8Path).lower().startswith("ibcmd") else "1cv8"
|
stub_engine = "ibcmd" if os.path.basename(args.V8Path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
arg_hints = {"/F": "-TempBasePath", "--db-path": "-TempBasePath"}
|
||||||
|
extra_args = resolve_extra_args(stub_engine, v8_extra, ibcmd_extra, arg_hints)
|
||||||
if stub_engine == "ibcmd":
|
if stub_engine == "ibcmd":
|
||||||
import shutil
|
import shutil
|
||||||
print(f'Creating infobase (ibcmd): {temp_base}')
|
print(f'Creating infobase (ibcmd): {temp_base}')
|
||||||
@@ -1044,7 +1308,8 @@ def main():
|
|||||||
if has_ref_types:
|
if has_ref_types:
|
||||||
ib_args += [f'--import={os.path.join(temp_base, "cfg")}', '--apply', '--force']
|
ib_args += [f'--import={os.path.join(temp_base, "cfg")}', '--apply', '--force']
|
||||||
ib_args.append(f'--data={ib_data}')
|
ib_args.append(f'--data={ib_data}')
|
||||||
result = subprocess.run(ib_args, capture_output=True, encoding='utf-8', errors='replace')
|
ib_args.extend(extra_args)
|
||||||
|
result = run_ibcmd(ib_args, warn_no_user=False)
|
||||||
shutil.rmtree(ib_data, ignore_errors=True)
|
shutil.rmtree(ib_data, ignore_errors=True)
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
if result.stdout:
|
if result.stdout:
|
||||||
@@ -1062,11 +1327,10 @@ def main():
|
|||||||
|
|
||||||
# Create infobase
|
# Create infobase
|
||||||
print(f'Creating infobase: {temp_base}')
|
print(f'Creating infobase: {temp_base}')
|
||||||
result = subprocess.run(
|
result = run_v8(args.V8Path, ['CREATEINFOBASE', f'File="{temp_base}"', '/DisableStartupDialogs']
|
||||||
[args.V8Path, 'CREATEINFOBASE', f'File={temp_base}', '/DisableStartupDialogs'],
|
+ [quote_if_needed(a) for a in extra_args])
|
||||||
capture_output=True, text=True,
|
|
||||||
)
|
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
|
print_platform_output(result)
|
||||||
print(f'Failed to create infobase (code: {result.returncode})', file=sys.stderr)
|
print(f'Failed to create infobase (code: {result.returncode})', file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@@ -1074,21 +1338,18 @@ def main():
|
|||||||
cfg_dir = os.path.join(temp_base, 'cfg')
|
cfg_dir = os.path.join(temp_base, 'cfg')
|
||||||
# LoadConfigFromFiles
|
# LoadConfigFromFiles
|
||||||
print('Loading configuration from files...')
|
print('Loading configuration from files...')
|
||||||
result = subprocess.run(
|
result = run_v8(args.V8Path, ['DESIGNER', f'/F"{temp_base}"', '/LoadConfigFromFiles', f'"{cfg_dir}"',
|
||||||
[args.V8Path, 'DESIGNER', f'/F{temp_base}', '/LoadConfigFromFiles', cfg_dir, '/DisableStartupDialogs'],
|
'/DisableStartupDialogs'] + [quote_if_needed(a) for a in extra_args])
|
||||||
capture_output=True, text=True,
|
|
||||||
)
|
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
|
print_platform_output(result)
|
||||||
print(f'Failed to load config (code: {result.returncode})', file=sys.stderr)
|
print(f'Failed to load config (code: {result.returncode})', file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# UpdateDBCfg
|
# UpdateDBCfg
|
||||||
print('Updating database configuration...')
|
print('Updating database configuration...')
|
||||||
update_log = os.path.join(tempfile.gettempdir(), 'stub_update_log.txt')
|
update_log = os.path.join(tempfile.gettempdir(), 'stub_update_log.txt')
|
||||||
result = subprocess.run(
|
result = run_v8(args.V8Path, ['DESIGNER', f'/F"{temp_base}"', '/UpdateDBCfg', '/Out', f'"{update_log}"',
|
||||||
[args.V8Path, 'DESIGNER', f'/F{temp_base}', '/UpdateDBCfg', '/Out', update_log, '/DisableStartupDialogs'],
|
'/DisableStartupDialogs'] + [quote_if_needed(a) for a in extra_args])
|
||||||
capture_output=True, text=True,
|
|
||||||
)
|
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
if os.path.isfile(update_log):
|
if os.path.isfile(update_log):
|
||||||
try:
|
try:
|
||||||
@@ -1096,6 +1357,7 @@ def main():
|
|||||||
print(f.read())
|
print(f.read())
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
print_platform_output(result)
|
||||||
print(f'Failed to update DB config (code: {result.returncode})', file=sys.stderr)
|
print(f'Failed to update DB config (code: {result.returncode})', file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" <па
|
|||||||
| `-InputFile <путь>` | да | Путь к EPF/ERF-файлу |
|
| `-InputFile <путь>` | да | Путь к EPF/ERF-файлу |
|
||||||
| `-OutputDir <путь>` | да | Каталог для выгрузки исходников |
|
| `-OutputDir <путь>` | да | Каталог для выгрузки исходников |
|
||||||
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
|
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
|
||||||
|
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
|
||||||
|
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
|
||||||
|
|
||||||
> `*` — обязательно хотя бы одно подключение. Без базы скрипт завершится с ошибкой (dump в пустой базе безвозвратно теряет ссылочные типы)
|
> `*` — обязательно хотя бы одно подключение. Без базы скрипт завершится с ошибкой (dump в пустой базе безвозвратно теряет ссылочные типы)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# epf-dump v1.4 — Dump external data processor or report (EPF/ERF) to XML sources
|
# epf-dump v1.11 — 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 не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Разборка внешней обработки/отчёта 1С в XML-исходники
|
Разборка внешней обработки/отчёта 1С в XML-исходники
|
||||||
@@ -35,6 +36,12 @@
|
|||||||
.PARAMETER Format
|
.PARAMETER Format
|
||||||
Формат выгрузки: Hierarchical или Plain (по умолчанию Hierarchical)
|
Формат выгрузки: Hierarchical или Plain (по умолчанию Hierarchical)
|
||||||
|
|
||||||
|
.PARAMETER AdditionalV8Arguments
|
||||||
|
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
|
||||||
|
|
||||||
|
.PARAMETER AdditionalIbcmdArguments
|
||||||
|
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
|
||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
.\epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build\МояОбработка.epf" -OutputDir "src"
|
.\epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build\МояОбработка.epf" -OutputDir "src"
|
||||||
|
|
||||||
@@ -70,12 +77,177 @@ param(
|
|||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[ValidateSet("Hierarchical", "Plain")]
|
[ValidateSet("Hierarchical", "Plain")]
|
||||||
[string]$Format = "Hierarchical"
|
[string]$Format = "Hierarchical",
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalV8Arguments = @(),
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalIbcmdArguments = @()
|
||||||
)
|
)
|
||||||
|
|
||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
$script:V8OwnedKeys = @(
|
||||||
|
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
|
||||||
|
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
|
||||||
|
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
|
||||||
|
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
|
||||||
|
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||||
|
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||||
|
)
|
||||||
|
$script:IbcmdOwnedKeys = @(
|
||||||
|
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||||
|
'--import', '--export', '--apply', '--force', '--create-database',
|
||||||
|
'--user', '--password'
|
||||||
|
)
|
||||||
|
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
|
||||||
|
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
|
||||||
|
|
||||||
|
function Test-ArgKeyMatch {
|
||||||
|
# A token matches a key when it equals the key, or starts with it and the next
|
||||||
|
# character is not a letter — catches glued /N"user" and --password=x, while
|
||||||
|
# keeping /ClearCache distinct from /C.
|
||||||
|
param([string]$Token, [string]$Key)
|
||||||
|
if ($Token.Length -lt $Key.Length) { return $false }
|
||||||
|
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
|
||||||
|
if ($Token.Length -eq $Key.Length) { return $true }
|
||||||
|
return -not [char]::IsLetter($Token[$Key.Length])
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ProjectExtraArgs {
|
||||||
|
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
|
||||||
|
param([string]$Name)
|
||||||
|
$dir = (Get-Location).Path
|
||||||
|
while ($dir) {
|
||||||
|
$pf = Join-Path $dir ".v8-project.json"
|
||||||
|
if (Test-Path $pf) {
|
||||||
|
try {
|
||||||
|
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
|
||||||
|
} catch {}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
$parent = Split-Path $dir -Parent
|
||||||
|
if (-not $parent -or $parent -eq $dir) { break }
|
||||||
|
$dir = $parent
|
||||||
|
}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Assert-ExtraArgs {
|
||||||
|
# The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
# output key fails with an opaque 1C error — reject what the skill owns itself.
|
||||||
|
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
|
||||||
|
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
|
||||||
|
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
|
||||||
|
foreach ($tok in $ExtraArgs) {
|
||||||
|
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
|
||||||
|
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
foreach ($k in $owned) {
|
||||||
|
if (Test-ArgKeyMatch $tok $k) {
|
||||||
|
$hint = ''
|
||||||
|
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
|
||||||
|
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-ExtraArgs {
|
||||||
|
# Pick the argument list for the selected engine and validate it. An explicitly passed
|
||||||
|
# parameter for the other engine is an error; the same keys coming from .v8-project.json
|
||||||
|
# simply do not apply — a project may describe both engines.
|
||||||
|
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
|
||||||
|
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
|
||||||
|
# space-separated values spill into positional ones, a comma-joined list arrives as a
|
||||||
|
# single token. So accept the repo's list convention (comma-separated) and split here;
|
||||||
|
# a native array call keeps working. A value containing a comma is not supported.
|
||||||
|
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -eq 'ibcmd') {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
|
||||||
|
} else {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
|
||||||
|
}
|
||||||
|
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
|
||||||
|
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
|
||||||
|
# would nest the array — the tokens would then be glued into one argument.
|
||||||
|
return $extra
|
||||||
|
}
|
||||||
|
|
||||||
|
function Format-ArgsForDisplay {
|
||||||
|
# Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
# a leaked password does.
|
||||||
|
param([string[]]$ArgList, [string]$Engine)
|
||||||
|
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
|
||||||
|
$res = @()
|
||||||
|
$maskNext = $false
|
||||||
|
foreach ($tok in $ArgList) {
|
||||||
|
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
|
||||||
|
$hit = $null
|
||||||
|
foreach ($k in $keys) {
|
||||||
|
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
|
||||||
|
}
|
||||||
|
if (-not $hit) { $res += $tok; continue }
|
||||||
|
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
|
||||||
|
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
|
||||||
|
else { $res += ($hit + '***') }
|
||||||
|
}
|
||||||
|
return ,$res
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-CleanPath {
|
||||||
|
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
# inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
|
||||||
|
param([string]$Value, [string]$ParamName)
|
||||||
|
if (-not $Value) { return $Value }
|
||||||
|
$v = $Value.Trim()
|
||||||
|
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
|
||||||
|
$v = $v.Substring(1, $v.Length - 2).Trim()
|
||||||
|
}
|
||||||
|
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
|
||||||
|
if ($v.Contains('"')) {
|
||||||
|
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
return $v
|
||||||
|
}
|
||||||
|
|
||||||
|
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
|
||||||
|
$InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath'
|
||||||
|
$InputFile = ConvertTo-CleanPath $InputFile '-InputFile'
|
||||||
|
$OutputDir = ConvertTo-CleanPath $OutputDir '-OutputDir'
|
||||||
|
|
||||||
|
function Assert-InfoBaseExists {
|
||||||
|
# These skills work on a ready infobase. Saying so up front beats the platform's
|
||||||
|
# "Неверные или отсутствующие параметры соединения" after a launch.
|
||||||
|
param([string]$Path)
|
||||||
|
if (-not $Path) { return }
|
||||||
|
if (-not (Test-Path (Join-Path $Path "1Cv8.1CD"))) {
|
||||||
|
Write-Host "Error: information base not found at $Path (no 1Cv8.1CD)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert-InfoBaseExists $InfoBasePath
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
function Find-ProjectV8Path {
|
||||||
$dir = (Get-Location).Path
|
$dir = (Get-Location).Path
|
||||||
@@ -106,7 +278,7 @@ if (-not $V8Path) {
|
|||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -115,7 +287,7 @@ if (Test-Path $V8Path -PathType Container) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +299,95 @@ if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
|
function ConvertFrom-PlatformBytes {
|
||||||
|
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
|
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
|
||||||
|
# one of them outright mangles Cyrillic.
|
||||||
|
param([byte[]]$Bytes)
|
||||||
|
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
|
||||||
|
try {
|
||||||
|
$strict = New-Object System.Text.UTF8Encoding($false, $true)
|
||||||
|
return $strict.GetString($Bytes)
|
||||||
|
} catch {
|
||||||
|
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-PlatformProcess {
|
||||||
|
# Run the platform non-interactively and capture its console output. A closed stdin pipe
|
||||||
|
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
|
||||||
|
# text out of our stream until we print it labelled (and out of the wrong encoding).
|
||||||
|
# Returns @{ Output; ExitCode }.
|
||||||
|
#
|
||||||
|
# Quoting differs by engine, so the caller says which it built:
|
||||||
|
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
|
||||||
|
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
|
||||||
|
# which is where 1C's own parser expects them; quoting again breaks the value.
|
||||||
|
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = if ($PreQuoted) {
|
||||||
|
$ProcArgs -join ' '
|
||||||
|
} else {
|
||||||
|
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
}
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
# stderr is drained in parallel: reading the streams one after another deadlocks
|
||||||
|
# as soon as the other one fills its pipe buffer.
|
||||||
|
$errMs = New-Object System.IO.MemoryStream
|
||||||
|
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
|
||||||
|
$outMs = New-Object System.IO.MemoryStream
|
||||||
|
$p.StandardOutput.BaseStream.CopyTo($outMs)
|
||||||
|
$errTask.Wait()
|
||||||
|
$p.WaitForExit()
|
||||||
|
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
|
||||||
|
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-PlatformOutput {
|
||||||
|
# Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
|
||||||
|
param([string]$Text)
|
||||||
|
if (-not $Text) { return }
|
||||||
|
$t = $Text.TrimEnd()
|
||||||
|
if (-not $t) { return }
|
||||||
|
$limit = 65536
|
||||||
|
if ($t.Length -gt $limit) {
|
||||||
|
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
|
||||||
|
}
|
||||||
|
Write-Host "--- Вывод платформы ---"
|
||||||
|
Write-Host $t
|
||||||
|
Write-Host "--- End ---"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function Test-DirNonEmpty {
|
||||||
|
# Postcondition: the platform must have written files into the output directory.
|
||||||
|
# Exit code 0 with an empty dir (broken/headless env) is a false success — reject it.
|
||||||
|
param([string]$Path)
|
||||||
|
return (Test-Path $Path -PathType Container) -and ([bool](Get-ChildItem -LiteralPath $Path -Force -ErrorAction SilentlyContinue | Select-Object -First 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
function Protect-Secrets {
|
||||||
|
# Redact literal secret values from a display string (String.Replace is literal, not regex).
|
||||||
|
param([string]$Text, [string[]]$Secrets)
|
||||||
|
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
|
||||||
|
return $Text
|
||||||
|
}
|
||||||
|
|
||||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' }
|
||||||
|
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
|
||||||
if ($engine -eq "ibcmd") {
|
if ($engine -eq "ibcmd") {
|
||||||
if (-not $InfoBasePath) {
|
if (-not $InfoBasePath) {
|
||||||
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
||||||
@@ -161,15 +421,21 @@ try {
|
|||||||
if ($UserName) { $arguments += "--user=$UserName" }
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
if ($Password) { $arguments += "--password=$Password" }
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
$arguments += "--data=$tempDir"
|
$arguments += "--data=$tempDir"
|
||||||
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
$arguments += $extraArgs
|
||||||
$output = & $V8Path @arguments 2>&1
|
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
||||||
$exitCode = $LASTEXITCODE
|
$__ib = Invoke-PlatformProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
$outMissing = ($exitCode -eq 0) -and -not (Test-DirNonEmpty $OutputDir)
|
||||||
|
if ($outMissing) { $exitCode = 1 }
|
||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
Write-Host "External data processor/report dumped successfully to: $OutputDir" -ForegroundColor Green
|
Write-Host "External data processor/report dumped successfully to: $OutputDir" -ForegroundColor Green
|
||||||
|
} elseif ($outMissing) {
|
||||||
|
Write-Host "Error: exit code 0 but no files under $OutputDir — dump produced no output" -ForegroundColor Red
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error dumping external data processor/report (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error dumping external data processor/report (code: $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
if ($output) { Write-Host ($output | Out-String) }
|
Write-PlatformOutput $output
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,15 +459,21 @@ try {
|
|||||||
$outFile = Join-Path $tempDir "dump_log.txt"
|
$outFile = Join-Path $tempDir "dump_log.txt"
|
||||||
$arguments += "/Out", "`"$outFile`""
|
$arguments += "/Out", "`"$outFile`""
|
||||||
$arguments += "/DisableStartupDialogs"
|
$arguments += "/DisableStartupDialogs"
|
||||||
|
$arguments += $extraArgs
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
|
||||||
$exitCode = $process.ExitCode
|
$exitCode = $__v8.ExitCode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
|
# Postcondition: exit 0 with an empty output directory is a false success.
|
||||||
|
$outMissing = ($exitCode -eq 0) -and -not (Test-DirNonEmpty $OutputDir)
|
||||||
|
if ($outMissing) { $exitCode = 1 }
|
||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
Write-Host "Dump completed successfully to: $OutputDir" -ForegroundColor Green
|
Write-Host "Dump completed successfully to: $OutputDir" -ForegroundColor Green
|
||||||
|
} elseif ($outMissing) {
|
||||||
|
Write-Host "Error: exit code 0 but no files under $OutputDir — dump produced no output" -ForegroundColor Red
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error dumping (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error dumping (code: $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
@@ -214,6 +486,7 @@ try {
|
|||||||
Write-Host "--- End ---"
|
Write-Host "--- End ---"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Write-PlatformOutput $__v8.Output
|
||||||
|
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# epf-dump v1.4 — Dump external data processor or report (EPF/ERF) to XML sources
|
# epf-dump v1.11 — 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
|
||||||
@@ -36,36 +36,323 @@ def _find_project_v8path():
|
|||||||
d = parent
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
V8_OWNED_KEYS = [
|
||||||
|
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
|
||||||
|
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
|
||||||
|
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
|
||||||
|
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
|
||||||
|
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||||
|
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||||
|
]
|
||||||
|
IBCMD_OWNED_KEYS = [
|
||||||
|
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||||
|
"--import", "--export", "--apply", "--force", "--create-database",
|
||||||
|
"--user", "--password",
|
||||||
|
]
|
||||||
|
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
|
||||||
|
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
|
||||||
|
|
||||||
|
|
||||||
|
def arg_key_match(token, key):
|
||||||
|
"""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
|
||||||
|
/ClearCache distinct from /C."""
|
||||||
|
if len(token) < len(key):
|
||||||
|
return False
|
||||||
|
if token[: len(key)].lower() != key.lower():
|
||||||
|
return False
|
||||||
|
if len(token) == len(key):
|
||||||
|
return True
|
||||||
|
return not token[len(key)].isalpha()
|
||||||
|
|
||||||
|
|
||||||
|
def project_extra_args(name):
|
||||||
|
"""v8args / ibcmdargs from .v8-project.json — same upward walk as v8path."""
|
||||||
|
d = os.getcwd()
|
||||||
|
while True:
|
||||||
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pf):
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
v = data.get(name)
|
||||||
|
if v:
|
||||||
|
return [str(x) for x in v]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return []
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return []
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def assert_extra_args(extra, engine, hints):
|
||||||
|
"""The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
output key fails with an opaque 1C error — reject what the skill owns itself."""
|
||||||
|
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
|
||||||
|
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
|
||||||
|
for tok in extra:
|
||||||
|
if engine == "ibcmd" and not tok.startswith("-"):
|
||||||
|
print(
|
||||||
|
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||||
|
f"({param} cannot extend the ibcmd command)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
for k in owned:
|
||||||
|
if arg_key_match(tok, k):
|
||||||
|
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||||
|
print(
|
||||||
|
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def format_args_for_display(arglist, engine):
|
||||||
|
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
a leaked password does."""
|
||||||
|
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
|
||||||
|
res = []
|
||||||
|
mask_next = False
|
||||||
|
for tok in arglist:
|
||||||
|
if mask_next:
|
||||||
|
res.append("***")
|
||||||
|
mask_next = False
|
||||||
|
continue
|
||||||
|
hit = None
|
||||||
|
for k in keys:
|
||||||
|
if tok[: len(k)].lower() == k.lower():
|
||||||
|
hit = k
|
||||||
|
break
|
||||||
|
if hit is None:
|
||||||
|
res.append(tok)
|
||||||
|
elif len(tok) == len(hit):
|
||||||
|
res.append(tok)
|
||||||
|
mask_next = True
|
||||||
|
elif tok[len(hit)] == "=":
|
||||||
|
res.append(hit + "=***")
|
||||||
|
else:
|
||||||
|
res.append(hit + "***")
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def extract_extra_args(argv, known_opts):
|
||||||
|
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
|
||||||
|
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
|
||||||
|
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
|
||||||
|
rest, v8, ibcmd = [], [], []
|
||||||
|
i = 0
|
||||||
|
while i < len(argv):
|
||||||
|
low = argv[i].lower()
|
||||||
|
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
|
||||||
|
target = v8 if low == "-additionalv8arguments" else ibcmd
|
||||||
|
i += 1
|
||||||
|
while i < len(argv) and argv[i].lower() not in known_opts:
|
||||||
|
target.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
rest.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
return rest, v8, ibcmd
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
||||||
|
"""Pick the argument list for the selected engine and validate it. An explicitly
|
||||||
|
passed parameter for the other engine is an error; the same keys coming from
|
||||||
|
.v8-project.json simply do not apply — a project may describe both engines.
|
||||||
|
|
||||||
|
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
|
||||||
|
so that form is the documented one and both ports must accept it. A value containing
|
||||||
|
a comma is not supported."""
|
||||||
|
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
|
||||||
|
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
|
||||||
|
if engine == "ibcmd" and v8_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||||
|
"(use -AdditionalIbcmdArguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine != "ibcmd" and ibcmd_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||||
|
"(use -AdditionalV8Arguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine == "ibcmd":
|
||||||
|
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
|
||||||
|
else:
|
||||||
|
extra = project_extra_args("v8args") + list(v8_extra)
|
||||||
|
if extra:
|
||||||
|
assert_extra_args(extra, engine, hints)
|
||||||
|
return extra
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
def _version_key(p):
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
"""Numeric sort key from version dir name."""
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def resolve_v8path(v8path):
|
||||||
"""Resolve path to 1cv8.exe."""
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
if not v8path:
|
if not v8path:
|
||||||
v8path = _find_project_v8path()
|
v8path = _find_project_v8path()
|
||||||
if not v8path:
|
if not v8path:
|
||||||
candidates = (
|
if os.name == "nt":
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
candidates = (
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
)
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
if candidates:
|
if candidates:
|
||||||
v8path = max(candidates, key=_version_key)
|
v8path = max(candidates, key=_version_key)
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
else:
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
if os.path.isdir(v8path):
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def decode_platform_bytes(data):
|
||||||
|
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
|
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — the locale
|
||||||
|
code page (what text=True uses) mangles both."""
|
||||||
|
if not data:
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
return data.decode("utf-8")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
return data.decode("cp866", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def assert_infobase_exists(path):
|
||||||
|
"""These skills work on a ready infobase. Saying so up front beats the platform's
|
||||||
|
"Неверные или отсутствующие параметры соединения" after a launch."""
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
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)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def clean_path(value, param=""):
|
||||||
|
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
|
||||||
|
if not value:
|
||||||
|
return value
|
||||||
|
v = value.strip()
|
||||||
|
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
|
||||||
|
v = v[1:-1].strip()
|
||||||
|
if len(v) > 3 and v[-1] in "\\/":
|
||||||
|
v = v[:-1]
|
||||||
|
if '"' in v:
|
||||||
|
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
def quote_if_needed(token):
|
||||||
|
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
|
||||||
|
verbatim, so a token with a space needs quotes of its own."""
|
||||||
|
if token and (" " in token or "\t" in token) and '"' not in token:
|
||||||
|
return f'"{token}"'
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
def run_v8(v8path, arguments):
|
||||||
|
"""Run 1cv8 in batch mode and capture its console output.
|
||||||
|
|
||||||
|
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
|
||||||
|
escape those quotes, so there the command line is handed over ready-made.
|
||||||
|
"""
|
||||||
|
if os.name == "nt":
|
||||||
|
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||||
|
else:
|
||||||
|
cmd = [v8path] + arguments
|
||||||
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def print_platform_output(result):
|
||||||
|
"""Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
|
||||||
|
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
|
||||||
|
if not text:
|
||||||
|
return
|
||||||
|
limit = 65536
|
||||||
|
if len(text) > limit:
|
||||||
|
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
|
||||||
|
print("--- Вывод платформы ---")
|
||||||
|
print(text)
|
||||||
|
print("--- End ---")
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def dir_nonempty(path):
|
||||||
|
"""Postcondition: the platform must have written files into the output directory.
|
||||||
|
Exit code 0 with an empty dir (broken/headless env) is a false success — reject it."""
|
||||||
|
return os.path.isdir(path) and any(os.scandir(path))
|
||||||
|
|
||||||
|
|
||||||
|
def _redact(text, *secrets):
|
||||||
|
"""Redact literal secret values (password, user) from a display string —
|
||||||
|
precise, never touches lookalike paths."""
|
||||||
|
for s in secrets:
|
||||||
|
if s:
|
||||||
|
text = text.replace(s, "***")
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
@@ -87,12 +374,36 @@ def main():
|
|||||||
choices=["Hierarchical", "Plain"],
|
choices=["Hierarchical", "Plain"],
|
||||||
help="Dump format (default: Hierarchical)",
|
help="Dump format (default: Hierarchical)",
|
||||||
)
|
)
|
||||||
args = parser.parse_args()
|
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
|
||||||
|
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
|
||||||
|
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
|
||||||
|
help="Extra ibcmd arguments in --key=value form")
|
||||||
|
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
||||||
|
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
||||||
|
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
||||||
|
assert_infobase_exists(args.InfoBasePath)
|
||||||
|
args.InputFile = clean_path(args.InputFile, "-InputFile")
|
||||||
|
args.OutputDir = clean_path(args.OutputDir, "-OutputDir")
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
arg_hints = {
|
||||||
|
"/F": "-InfoBasePath",
|
||||||
|
"/S": "-InfoBaseServer + -InfoBaseRef",
|
||||||
|
"/N": "-UserName",
|
||||||
|
"/P": "-Password",
|
||||||
|
"--db-path": "-InfoBasePath",
|
||||||
|
"--user": "-UserName",
|
||||||
|
"--password": "-Password",
|
||||||
|
}
|
||||||
|
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
|
||||||
|
|
||||||
# --- 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", file=sys.stderr)
|
||||||
@@ -130,51 +441,57 @@ def main():
|
|||||||
if args.Password:
|
if args.Password:
|
||||||
arguments.append(f"--password={args.Password}")
|
arguments.append(f"--password={args.Password}")
|
||||||
arguments.append(f"--data={ib_data}")
|
arguments.append(f"--data={ib_data}")
|
||||||
print(f"Running: ibcmd {' '.join(arguments)}")
|
arguments.extend(extra_args)
|
||||||
result = subprocess.run([v8path] + arguments, capture_output=True, encoding="utf-8", errors="replace")
|
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||||
if result.returncode == 0:
|
result = run_ibcmd([v8path] + arguments, warn_no_user=False)
|
||||||
|
exit_code = result.returncode
|
||||||
|
out_missing = exit_code == 0 and not dir_nonempty(args.OutputDir)
|
||||||
|
if out_missing:
|
||||||
|
exit_code = 1
|
||||||
|
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:
|
||||||
|
print(f"Error: exit code 0 but no files under {args.OutputDir} — dump produced no output", file=sys.stderr)
|
||||||
else:
|
else:
|
||||||
print(f"Error dumping external data processor/report (code: {result.returncode})", file=sys.stderr)
|
print(f"Error dumping external data processor/report (code: {exit_code})", file=sys.stderr)
|
||||||
if result.stdout:
|
sys.exit(exit_code)
|
||||||
print(result.stdout)
|
|
||||||
if result.stderr:
|
|
||||||
print(result.stderr, file=sys.stderr)
|
|
||||||
sys.exit(result.returncode)
|
|
||||||
|
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
arguments = ["DESIGNER"]
|
arguments = ["DESIGNER"]
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
if args.InfoBaseServer and args.InfoBaseRef:
|
||||||
arguments += ["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"]
|
arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
|
||||||
else:
|
else:
|
||||||
arguments += ["/F", args.InfoBasePath]
|
arguments += ["/F", f'"{args.InfoBasePath}"']
|
||||||
|
|
||||||
if args.UserName:
|
if args.UserName:
|
||||||
arguments.append(f"/N{args.UserName}")
|
arguments.append(f'/N"{args.UserName}"')
|
||||||
if args.Password:
|
if args.Password:
|
||||||
arguments.append(f"/P{args.Password}")
|
arguments.append(f'/P"{args.Password}"')
|
||||||
|
|
||||||
arguments += ["/DumpExternalDataProcessorOrReportToFiles", args.OutputDir, args.InputFile]
|
arguments += ["/DumpExternalDataProcessorOrReportToFiles", f'"{args.OutputDir}"', f'"{args.InputFile}"']
|
||||||
arguments += ["-Format", args.Format]
|
arguments += ["-Format", args.Format]
|
||||||
|
|
||||||
# --- Output ---
|
# --- Output ---
|
||||||
out_file = os.path.join(temp_dir, "dump_log.txt")
|
out_file = os.path.join(temp_dir, "dump_log.txt")
|
||||||
arguments += ["/Out", out_file]
|
arguments += ["/Out", f'"{out_file}"']
|
||||||
arguments.append("/DisableStartupDialogs")
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
arguments.extend(quote_if_needed(a) for a in extra_args)
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||||
result = subprocess.run(
|
result = run_v8(v8path, arguments)
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
exit_code = result.returncode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
|
# Postcondition: exit 0 with an empty output directory is a false success.
|
||||||
|
out_missing = exit_code == 0 and not dir_nonempty(args.OutputDir)
|
||||||
|
if out_missing:
|
||||||
|
exit_code = 1
|
||||||
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:
|
||||||
|
print(f"Error: exit code 0 but no files under {args.OutputDir} — dump produced no output", file=sys.stderr)
|
||||||
else:
|
else:
|
||||||
print(f"Error dumping (code: {exit_code})", file=sys.stderr)
|
print(f"Error dumping (code: {exit_code})", file=sys.stderr)
|
||||||
|
|
||||||
@@ -189,6 +506,7 @@ def main():
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
print_platform_output(result)
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# epf-validate v1.2 — Validate 1C external data processor / report structure
|
# epf-validate v1.3 — 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
|
||||||
param(
|
param(
|
||||||
@@ -185,8 +185,9 @@ if ($root.NamespaceURI -ne $expectedNs) {
|
|||||||
$version = $root.GetAttribute("version")
|
$version = $root.GetAttribute("version")
|
||||||
if (-not $version) {
|
if (-not $version) {
|
||||||
Report-Warn "1. Missing version attribute on MetaDataObject"
|
Report-Warn "1. Missing version attribute on MetaDataObject"
|
||||||
} elseif ($version -ne "2.17" -and $version -ne "2.20" -and $version -ne "2.21") {
|
} elseif ($version -notin @("2.17", "2.18", "2.19", "2.20", "2.21")) {
|
||||||
Report-Warn "1. Unusual version '$version' (expected 2.17, 2.20 or 2.21)"
|
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27).
|
||||||
|
Report-Warn "1. Unusual version '$version' (expected 2.17-2.20 or 2.21)"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Detect type: ExternalDataProcessor or ExternalReport
|
# Detect type: ExternalDataProcessor or ExternalReport
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# epf-validate v1.2 — Validate 1C external data processor / report structure
|
# epf-validate v1.3 — 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
|
||||||
|
|
||||||
@@ -165,8 +165,9 @@ def main():
|
|||||||
version = root.get("version", "")
|
version = root.get("version", "")
|
||||||
if not version:
|
if not version:
|
||||||
report_warn("1. Missing version attribute on MetaDataObject")
|
report_warn("1. Missing version attribute on MetaDataObject")
|
||||||
elif version not in ("2.17", "2.20", "2.21"):
|
elif version not in ("2.17", "2.18", "2.19", "2.20", "2.21"):
|
||||||
report_warn(f"1. Unusual version '{version}' (expected 2.17, 2.20 or 2.21)")
|
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27).
|
||||||
|
report_warn(f"1. Unusual version '{version}' (expected 2.17-2.20 or 2.21)")
|
||||||
|
|
||||||
# Detect type
|
# Detect type
|
||||||
child_elements = []
|
child_elements = []
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-bu
|
|||||||
| `-Password <пароль>` | нет | Пароль |
|
| `-Password <пароль>` | нет | Пароль |
|
||||||
| `-SourceFile <путь>` | да | Путь к корневому XML-файлу исходников |
|
| `-SourceFile <путь>` | да | Путь к корневому XML-файлу исходников |
|
||||||
| `-OutputFile <путь>` | да | Путь к выходному ERF-файлу |
|
| `-OutputFile <путь>` | да | Путь к выходному ERF-файлу |
|
||||||
|
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
|
||||||
|
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
|
||||||
|
|
||||||
> `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных
|
> `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dum
|
|||||||
| `-InputFile <путь>` | да | Путь к ERF-файлу |
|
| `-InputFile <путь>` | да | Путь к ERF-файлу |
|
||||||
| `-OutputDir <путь>` | да | Каталог для выгрузки исходников |
|
| `-OutputDir <путь>` | да | Каталог для выгрузки исходников |
|
||||||
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
|
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
|
||||||
|
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
|
||||||
|
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
|
||||||
|
|
||||||
> `*` — обязательно хотя бы одно подключение. Без базы скрипт завершится с ошибкой (dump в пустой базе безвозвратно теряет ссылочные типы)
|
> `*` — обязательно хотя бы одно подключение. Без базы скрипт завершится с ошибкой (dump в пустой базе безвозвратно теряет ссылочные типы)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# form-add v1.7 — Add managed form to 1C config object
|
# form-add v1.12 — 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
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -33,6 +33,16 @@ function Get-RootUuid([string]$xmlPath) {
|
|||||||
} catch {}
|
} catch {}
|
||||||
return $null
|
return $null
|
||||||
}
|
}
|
||||||
|
function Test-ExternalObjectRoot([string]$xmlPath) {
|
||||||
|
if (-not (Test-Path $xmlPath)) { return $false }
|
||||||
|
try {
|
||||||
|
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||||
|
$el = $mx.DocumentElement.FirstChild
|
||||||
|
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||||
|
if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
|
||||||
|
} catch {}
|
||||||
|
return $false
|
||||||
|
}
|
||||||
function Find-V8Project([string]$startDir) {
|
function Find-V8Project([string]$startDir) {
|
||||||
$d = $startDir
|
$d = $startDir
|
||||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||||
@@ -69,10 +79,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
|||||||
try {
|
try {
|
||||||
$rp = $targetPath
|
$rp = $targetPath
|
||||||
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
||||||
|
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||||
|
if (Test-ExternalObjectRoot $rp) { return }
|
||||||
$elemUuid = Get-RootUuid $rp
|
$elemUuid = Get-RootUuid $rp
|
||||||
$cfgDir = $null; $binPath = $null
|
$cfgDir = $null; $binPath = $null
|
||||||
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
||||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||||
|
if (Test-ExternalObjectRoot "$d.xml") { return }
|
||||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||||
if (-not $cfgDir) {
|
if (-not $cfgDir) {
|
||||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||||
@@ -143,7 +156,10 @@ function Detect-FormatVersion([string]$dir) {
|
|||||||
while ($d) {
|
while ($d) {
|
||||||
$cfgPath = Join-Path $d "Configuration.xml"
|
$cfgPath = Join-Path $d "Configuration.xml"
|
||||||
if (Test-Path $cfgPath) {
|
if (Test-Path $cfgPath) {
|
||||||
$head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length))
|
$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] }
|
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
||||||
}
|
}
|
||||||
$parent = Split-Path $d -Parent
|
$parent = Split-Path $d -Parent
|
||||||
@@ -195,7 +211,7 @@ $supportedTypes = @(
|
|||||||
"Document", "Catalog", "DataProcessor", "Report",
|
"Document", "Catalog", "DataProcessor", "Report",
|
||||||
"ExternalDataProcessor", "ExternalReport",
|
"ExternalDataProcessor", "ExternalReport",
|
||||||
"InformationRegister", "AccumulationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
|
"InformationRegister", "AccumulationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
|
||||||
"ExchangePlan", "BusinessProcess", "Task"
|
"ExchangePlan", "BusinessProcess", "Task", "DocumentJournal"
|
||||||
)
|
)
|
||||||
|
|
||||||
$objectType = $null
|
$objectType = $null
|
||||||
@@ -471,7 +487,10 @@ if (-not $childObjects) {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Добавить <Form>$FormName</Form>
|
# Добавить <Form>$FormName</Form> — идемпотентно (не дублировать уже зарегистрированную)
|
||||||
|
$alreadyRegistered = [bool]$childObjects.SelectSingleNode("md:Form[text()='$FormName']", $nsMgr)
|
||||||
|
|
||||||
|
if (-not $alreadyRegistered) {
|
||||||
$formElem = $xmlDoc.CreateElement("Form", "http://v8.1c.ru/8.3/MDClasses")
|
$formElem = $xmlDoc.CreateElement("Form", "http://v8.1c.ru/8.3/MDClasses")
|
||||||
$formElem.InnerText = $FormName
|
$formElem.InnerText = $FormName
|
||||||
|
|
||||||
@@ -525,6 +544,7 @@ if ($insertBefore) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# --- SetDefault ---
|
# --- SetDefault ---
|
||||||
|
|
||||||
@@ -590,7 +610,11 @@ Write-Host " Metadata: $objDirName\$objBaseName\Forms\$FormName.xml"
|
|||||||
Write-Host " Form: $objDirName\$objBaseName\Forms\$FormName\Ext\Form.xml"
|
Write-Host " Form: $objDirName\$objBaseName\Forms\$FormName\Ext\Form.xml"
|
||||||
Write-Host " Module: $objDirName\$objBaseName\Forms\$FormName\Ext\Form\Module.bsl"
|
Write-Host " Module: $objDirName\$objBaseName\Forms\$FormName\Ext\Form\Module.bsl"
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "Registered: <Form>$FormName</Form> in ChildObjects"
|
if ($alreadyRegistered) {
|
||||||
|
Write-Host "Already registered: <Form>$FormName</Form> in ChildObjects (skipped duplicate)"
|
||||||
|
} else {
|
||||||
|
Write-Host "Registered: <Form>$FormName</Form> in ChildObjects"
|
||||||
|
}
|
||||||
if ($defaultUpdated) {
|
if ($defaultUpdated) {
|
||||||
Write-Host "${defaultPropName}: $defaultValue"
|
Write-Host "${defaultPropName}: $defaultValue"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# form-add v1.7 — Add managed form to 1C config object
|
# form-add v1.12 — 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
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -32,6 +32,18 @@ def _sg_root_uuid(xml_path):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _sg_is_external_root(xml_path):
|
||||||
|
if not os.path.isfile(xml_path):
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
mx = etree.parse(xml_path).getroot()
|
||||||
|
for child in mx:
|
||||||
|
if isinstance(child.tag, str):
|
||||||
|
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
return False
|
||||||
|
|
||||||
def _sg_find_v8project(start_dir):
|
def _sg_find_v8project(start_dir):
|
||||||
d = start_dir
|
d = start_dir
|
||||||
for _ in range(20):
|
for _ in range(20):
|
||||||
@@ -71,6 +83,9 @@ def _sg_get_edit_mode(cfg_dir):
|
|||||||
def assert_edit_allowed(target_path, require):
|
def assert_edit_allowed(target_path, require):
|
||||||
try:
|
try:
|
||||||
rp = os.path.abspath(target_path)
|
rp = os.path.abspath(target_path)
|
||||||
|
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||||
|
if _sg_is_external_root(rp):
|
||||||
|
return
|
||||||
elem_uuid = _sg_root_uuid(rp)
|
elem_uuid = _sg_root_uuid(rp)
|
||||||
cfg_dir = None
|
cfg_dir = None
|
||||||
bin_path = None
|
bin_path = None
|
||||||
@@ -78,6 +93,8 @@ def assert_edit_allowed(target_path, require):
|
|||||||
for _ in range(12):
|
for _ in range(12):
|
||||||
if not d:
|
if not d:
|
||||||
break
|
break
|
||||||
|
if _sg_is_external_root(d + ".xml"):
|
||||||
|
return
|
||||||
if not elem_uuid:
|
if not elem_uuid:
|
||||||
elem_uuid = _sg_root_uuid(d + ".xml")
|
elem_uuid = _sg_root_uuid(d + ".xml")
|
||||||
if not cfg_dir:
|
if not cfg_dir:
|
||||||
@@ -193,14 +210,50 @@ def detect_format_version(d):
|
|||||||
return "2.17"
|
return "2.17"
|
||||||
|
|
||||||
|
|
||||||
def save_xml_with_bom(tree, path):
|
def _detect_xml_style(path):
|
||||||
"""Save XML tree to file with UTF-8 BOM."""
|
"""Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
|
||||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
финальный перенос. None → файл новый (сохранить текущее поведение)."""
|
||||||
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
try:
|
||||||
if not xml_bytes.endswith(b"\n"):
|
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)."""
|
||||||
|
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"))
|
||||||
|
# Финальный перенос — как в оригинале (новый файл → есть)
|
||||||
|
want_final_nl = style["final_nl"] if style else True
|
||||||
|
xml_bytes = xml_bytes.rstrip(b"\n")
|
||||||
|
if want_final_nl:
|
||||||
xml_bytes += b"\n"
|
xml_bytes += b"\n"
|
||||||
|
# EOL — как в оригинале (новый файл → LF, текущее поведение)
|
||||||
|
if style and style["crlf"]:
|
||||||
|
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:
|
with open(path, "wb") as f:
|
||||||
f.write(b"\xef\xbb\xbf")
|
if style is None or style["bom"]:
|
||||||
|
f.write(b"\xef\xbb\xbf")
|
||||||
f.write(xml_bytes)
|
f.write(xml_bytes)
|
||||||
|
|
||||||
|
|
||||||
@@ -256,7 +309,7 @@ def main():
|
|||||||
"Document", "Catalog", "DataProcessor", "Report",
|
"Document", "Catalog", "DataProcessor", "Report",
|
||||||
"ExternalDataProcessor", "ExternalReport",
|
"ExternalDataProcessor", "ExternalReport",
|
||||||
"InformationRegister", "AccumulationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
|
"InformationRegister", "AccumulationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
|
||||||
"ExchangePlan", "BusinessProcess", "Task",
|
"ExchangePlan", "BusinessProcess", "Task", "DocumentJournal",
|
||||||
]
|
]
|
||||||
|
|
||||||
object_type = None
|
object_type = None
|
||||||
@@ -539,47 +592,50 @@ def main():
|
|||||||
print(f"Не найден элемент ChildObjects в {object_path}", file=sys.stderr)
|
print(f"Не найден элемент ChildObjects в {object_path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Add <Form>$FormName</Form>
|
# Add <Form>$FormName</Form> — idempotent (do not duplicate already-registered form)
|
||||||
form_elem = etree.Element(f"{{{ns}}}Form")
|
already_registered = child_objects.find(f"md:Form[.='{form_name}']", NSMAP) is not None
|
||||||
form_elem.text = form_name
|
|
||||||
|
|
||||||
# Find first <Template> to insert before it
|
if not already_registered:
|
||||||
first_template = child_objects.find("md:Template", NSMAP)
|
form_elem = etree.Element(f"{{{ns}}}Form")
|
||||||
# Find first <TabularSection> to insert before it (if no Template)
|
form_elem.text = form_name
|
||||||
first_tabular = child_objects.find("md:TabularSection", NSMAP)
|
|
||||||
|
|
||||||
# Determine insertion point: before Template, before TabularSection, or at end
|
# Find first <Template> to insert before it
|
||||||
insert_before = None
|
first_template = child_objects.find("md:Template", NSMAP)
|
||||||
if first_template is not None:
|
# Find first <TabularSection> to insert before it (if no Template)
|
||||||
insert_before = first_template
|
first_tabular = child_objects.find("md:TabularSection", NSMAP)
|
||||||
elif first_tabular is not None:
|
|
||||||
insert_before = first_tabular
|
|
||||||
|
|
||||||
if insert_before is not None:
|
# Determine insertion point: before Template, before TabularSection, or at end
|
||||||
# Insert before the found element
|
insert_before = None
|
||||||
idx = list(child_objects).index(insert_before)
|
if first_template is not None:
|
||||||
child_objects.insert(idx, form_elem)
|
insert_before = first_template
|
||||||
# Whitespace: form_elem gets "\n\t\t\t" as tail (indent before insert_before)
|
elif first_tabular is not None:
|
||||||
form_elem.tail = "\n\t\t\t"
|
insert_before = first_tabular
|
||||||
else:
|
|
||||||
# Add to end of ChildObjects
|
if insert_before is not None:
|
||||||
children = list(child_objects)
|
# Insert before the found element
|
||||||
if len(children) == 0 and (child_objects.text is None or child_objects.text.strip() == ""):
|
idx = list(child_objects).index(insert_before)
|
||||||
# Empty ChildObjects (self-closing)
|
child_objects.insert(idx, form_elem)
|
||||||
child_objects.text = "\n\t\t\t"
|
# Whitespace: form_elem gets "\n\t\t\t" as tail (indent before insert_before)
|
||||||
child_objects.append(form_elem)
|
form_elem.tail = "\n\t\t\t"
|
||||||
form_elem.tail = "\n\t\t"
|
|
||||||
else:
|
else:
|
||||||
if len(children) > 0:
|
# Add to end of ChildObjects
|
||||||
last_child = children[-1]
|
children = list(child_objects)
|
||||||
old_tail = last_child.tail
|
if len(children) == 0 and (child_objects.text is None or child_objects.text.strip() == ""):
|
||||||
last_child.tail = "\n\t\t\t"
|
# Empty ChildObjects (self-closing)
|
||||||
child_objects.append(form_elem)
|
child_objects.text = "\n\t\t\t"
|
||||||
form_elem.tail = old_tail if old_tail else "\n\t\t"
|
|
||||||
else:
|
|
||||||
child_objects.text = (child_objects.text or "") + "\n\t\t\t"
|
|
||||||
child_objects.append(form_elem)
|
child_objects.append(form_elem)
|
||||||
form_elem.tail = "\n\t\t"
|
form_elem.tail = "\n\t\t"
|
||||||
|
else:
|
||||||
|
if len(children) > 0:
|
||||||
|
last_child = children[-1]
|
||||||
|
old_tail = last_child.tail
|
||||||
|
last_child.tail = "\n\t\t\t"
|
||||||
|
child_objects.append(form_elem)
|
||||||
|
form_elem.tail = old_tail if old_tail else "\n\t\t"
|
||||||
|
else:
|
||||||
|
child_objects.text = (child_objects.text or "") + "\n\t\t\t"
|
||||||
|
child_objects.append(form_elem)
|
||||||
|
form_elem.tail = "\n\t\t"
|
||||||
|
|
||||||
# --- SetDefault ---
|
# --- SetDefault ---
|
||||||
|
|
||||||
@@ -624,7 +680,10 @@ def main():
|
|||||||
print(f" Form: {obj_dir_name}\\{obj_base_name}\\Forms\\{form_name}\\Ext\\Form.xml")
|
print(f" Form: {obj_dir_name}\\{obj_base_name}\\Forms\\{form_name}\\Ext\\Form.xml")
|
||||||
print(f" Module: {obj_dir_name}\\{obj_base_name}\\Forms\\{form_name}\\Ext\\Form\\Module.bsl")
|
print(f" Module: {obj_dir_name}\\{obj_base_name}\\Forms\\{form_name}\\Ext\\Form\\Module.bsl")
|
||||||
print()
|
print()
|
||||||
print(f"Registered: <Form>{form_name}</Form> in ChildObjects")
|
if already_registered:
|
||||||
|
print(f"Already registered: <Form>{form_name}</Form> in ChildObjects (skipped duplicate)")
|
||||||
|
else:
|
||||||
|
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}")
|
||||||
print()
|
print()
|
||||||
|
|||||||
@@ -187,6 +187,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-compile.ps1" -
|
|||||||
| `showTitle: true` | Показывать заголовок группы |
|
| `showTitle: true` | Показывать заголовок группы |
|
||||||
| `united: false` | Левый край полей ввода выравнивается только в пределах этой группы (по умолчанию `true` — сквозное выравнивание по самому длинному заголовку, в т.ч. с соседними группами) |
|
| `united: false` | Левый край полей ввода выравнивается только в пределах этой группы (по умолчанию `true` — сквозное выравнивание по самому длинному заголовку, в т.ч. с соседними группами) |
|
||||||
| `collapsed: true` | Для `behavior: "collapsible"` / `"popup"` — группа создаётся свёрнутой |
|
| `collapsed: true` | Для `behavior: "collapsible"` / `"popup"` — группа создаётся свёрнутой |
|
||||||
|
| `controlRepresentation` | Отображение управления свёрткой (`behavior: "collapsible"`): `"TitleHyperlink"` (гиперссылка заголовка, по умолчанию) / `"Picture"` (картинка) |
|
||||||
| `representation` | `"none"`, `"normal"`, `"weak"`, `"strong"` |
|
| `representation` | `"none"`, `"normal"`, `"weak"`, `"strong"` |
|
||||||
| `children: [...]` | Вложенные элементы |
|
| `children: [...]` | Вложенные элементы |
|
||||||
|
|
||||||
@@ -549,8 +550,8 @@ PictureField, привязанный к булеву/числу, рисует и
|
|||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
1. **Компиляция**: `/form-compile` генерирует `Form.xml` и автоматически регистрирует `<Form>` в `ChildObjects` родительского объекта (если OutputPath следует конвенции `.../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml`).
|
1. **Каркас**: `/form-add` создаёт метаданные формы (`ФормаСписка.xml`), `Module.bsl` и регистрирует форму у объекта.
|
||||||
2. **Метаданные формы** (`ФормаСписка.xml`) и `Module.bsl` создаёт `/form-add`. Если `/form-add` ещё не вызывался — вызови после `/form-compile`. Он не перезаписывает существующий Form.xml.
|
2. **Компиляция**: `/form-compile` наполняет `Form.xml` элементами.
|
||||||
3. **Проверка**: `/form-validate`, `/form-info`.
|
3. **Проверка**: `/form-validate`, `/form-info`.
|
||||||
|
|
||||||
## Верификация
|
## Верификация
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# form-compile v1.174 — Compile 1C managed form from JSON or object metadata
|
# form-compile v1.176 — Compile 1C managed form from JSON or object metadata
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[string]$JsonPath,
|
[string]$JsonPath,
|
||||||
@@ -1337,7 +1337,10 @@ function Detect-FormatVersion([string]$dir) {
|
|||||||
while ($d) {
|
while ($d) {
|
||||||
$cfgPath = Join-Path $d "Configuration.xml"
|
$cfgPath = Join-Path $d "Configuration.xml"
|
||||||
if (Test-Path $cfgPath) {
|
if (Test-Path $cfgPath) {
|
||||||
$head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length))
|
$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] }
|
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
||||||
}
|
}
|
||||||
$parent = Split-Path $d -Parent
|
$parent = Split-Path $d -Parent
|
||||||
@@ -1362,6 +1365,16 @@ function Get-RootUuid([string]$xmlPath) {
|
|||||||
} catch {}
|
} catch {}
|
||||||
return $null
|
return $null
|
||||||
}
|
}
|
||||||
|
function Test-ExternalObjectRoot([string]$xmlPath) {
|
||||||
|
if (-not (Test-Path $xmlPath)) { return $false }
|
||||||
|
try {
|
||||||
|
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||||
|
$el = $mx.DocumentElement.FirstChild
|
||||||
|
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||||
|
if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
|
||||||
|
} catch {}
|
||||||
|
return $false
|
||||||
|
}
|
||||||
function Find-V8Project([string]$startDir) {
|
function Find-V8Project([string]$startDir) {
|
||||||
$d = $startDir
|
$d = $startDir
|
||||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||||
@@ -1398,10 +1411,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
|||||||
try {
|
try {
|
||||||
$rp = $targetPath
|
$rp = $targetPath
|
||||||
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
||||||
|
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||||
|
if (Test-ExternalObjectRoot $rp) { return }
|
||||||
$elemUuid = Get-RootUuid $rp
|
$elemUuid = Get-RootUuid $rp
|
||||||
$cfgDir = $null; $binPath = $null
|
$cfgDir = $null; $binPath = $null
|
||||||
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
||||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||||
|
if (Test-ExternalObjectRoot "$d.xml") { return }
|
||||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||||
if (-not $cfgDir) {
|
if (-not $cfgDir) {
|
||||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# form-compile v1.174 — Compile 1C managed form from JSON or object metadata
|
# form-compile v1.176 — Compile 1C managed form from JSON or object metadata
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
import argparse
|
import argparse
|
||||||
import copy
|
import copy
|
||||||
@@ -34,6 +34,18 @@ def _sg_root_uuid(xml_path):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _sg_is_external_root(xml_path):
|
||||||
|
if not os.path.isfile(xml_path):
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
mx = etree.parse(xml_path).getroot()
|
||||||
|
for child in mx:
|
||||||
|
if isinstance(child.tag, str):
|
||||||
|
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
return False
|
||||||
|
|
||||||
def _sg_find_v8project(start_dir):
|
def _sg_find_v8project(start_dir):
|
||||||
d = start_dir
|
d = start_dir
|
||||||
for _ in range(20):
|
for _ in range(20):
|
||||||
@@ -73,6 +85,9 @@ def _sg_get_edit_mode(cfg_dir):
|
|||||||
def assert_edit_allowed(target_path, require):
|
def assert_edit_allowed(target_path, require):
|
||||||
try:
|
try:
|
||||||
rp = os.path.abspath(target_path)
|
rp = os.path.abspath(target_path)
|
||||||
|
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||||
|
if _sg_is_external_root(rp):
|
||||||
|
return
|
||||||
elem_uuid = _sg_root_uuid(rp)
|
elem_uuid = _sg_root_uuid(rp)
|
||||||
cfg_dir = None
|
cfg_dir = None
|
||||||
bin_path = None
|
bin_path = None
|
||||||
@@ -80,6 +95,8 @@ def assert_edit_allowed(target_path, require):
|
|||||||
for _ in range(12):
|
for _ in range(12):
|
||||||
if not d:
|
if not d:
|
||||||
break
|
break
|
||||||
|
if _sg_is_external_root(d + ".xml"):
|
||||||
|
return
|
||||||
if not elem_uuid:
|
if not elem_uuid:
|
||||||
elem_uuid = _sg_root_uuid(d + ".xml")
|
elem_uuid = _sg_root_uuid(d + ".xml")
|
||||||
if not cfg_dir:
|
if not cfg_dir:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# form-edit v1.3 — Edit 1C managed form elements
|
# form-edit v1.6 — Edit 1C managed form elements
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -27,6 +27,16 @@ function Get-RootUuid([string]$xmlPath) {
|
|||||||
} catch {}
|
} catch {}
|
||||||
return $null
|
return $null
|
||||||
}
|
}
|
||||||
|
function Test-ExternalObjectRoot([string]$xmlPath) {
|
||||||
|
if (-not (Test-Path $xmlPath)) { return $false }
|
||||||
|
try {
|
||||||
|
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||||
|
$el = $mx.DocumentElement.FirstChild
|
||||||
|
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||||
|
if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
|
||||||
|
} catch {}
|
||||||
|
return $false
|
||||||
|
}
|
||||||
function Find-V8Project([string]$startDir) {
|
function Find-V8Project([string]$startDir) {
|
||||||
$d = $startDir
|
$d = $startDir
|
||||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||||
@@ -63,10 +73,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
|||||||
try {
|
try {
|
||||||
$rp = $targetPath
|
$rp = $targetPath
|
||||||
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
||||||
|
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||||
|
if (Test-ExternalObjectRoot $rp) { return }
|
||||||
$elemUuid = Get-RootUuid $rp
|
$elemUuid = Get-RootUuid $rp
|
||||||
$cfgDir = $null; $binPath = $null
|
$cfgDir = $null; $binPath = $null
|
||||||
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
||||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||||
|
if (Test-ExternalObjectRoot "$d.xml") { return }
|
||||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||||
if (-not $cfgDir) {
|
if (-not $cfgDir) {
|
||||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||||
@@ -257,8 +270,11 @@ function X {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Esc-Xml {
|
function Esc-Xml {
|
||||||
|
# Экранирование ТЕКСТА элемента: только & < > . Кавычки в тексте платформа НЕ экранирует —
|
||||||
|
# пишет литерально (проверено: 92142 сырых кавычки на корпус, ни одной "). " платформа
|
||||||
|
# принимает, но при выгрузке нормализует обратно в кавычку → лишний шум в роундтрипе.
|
||||||
param([string]$s)
|
param([string]$s)
|
||||||
return $s.Replace('&','&').Replace('<','<').Replace('>','>').Replace('"','"')
|
return $s.Replace('&','&').Replace('<','<').Replace('>','>')
|
||||||
}
|
}
|
||||||
|
|
||||||
function Emit-MLText {
|
function Emit-MLText {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# form-edit v1.3 — Edit 1C managed form elements (Python port)
|
# form-edit v1.6 — Edit 1C managed form elements (Python port)
|
||||||
# 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 json
|
||||||
@@ -31,6 +31,18 @@ def _sg_root_uuid(xml_path):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _sg_is_external_root(xml_path):
|
||||||
|
if not os.path.isfile(xml_path):
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
mx = etree.parse(xml_path).getroot()
|
||||||
|
for child in mx:
|
||||||
|
if isinstance(child.tag, str):
|
||||||
|
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
return False
|
||||||
|
|
||||||
def _sg_find_v8project(start_dir):
|
def _sg_find_v8project(start_dir):
|
||||||
d = start_dir
|
d = start_dir
|
||||||
for _ in range(20):
|
for _ in range(20):
|
||||||
@@ -70,6 +82,9 @@ def _sg_get_edit_mode(cfg_dir):
|
|||||||
def assert_edit_allowed(target_path, require):
|
def assert_edit_allowed(target_path, require):
|
||||||
try:
|
try:
|
||||||
rp = os.path.abspath(target_path)
|
rp = os.path.abspath(target_path)
|
||||||
|
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||||
|
if _sg_is_external_root(rp):
|
||||||
|
return
|
||||||
elem_uuid = _sg_root_uuid(rp)
|
elem_uuid = _sg_root_uuid(rp)
|
||||||
cfg_dir = None
|
cfg_dir = None
|
||||||
bin_path = None
|
bin_path = None
|
||||||
@@ -77,6 +92,8 @@ def assert_edit_allowed(target_path, require):
|
|||||||
for _ in range(12):
|
for _ in range(12):
|
||||||
if not d:
|
if not d:
|
||||||
break
|
break
|
||||||
|
if _sg_is_external_root(d + ".xml"):
|
||||||
|
return
|
||||||
if not elem_uuid:
|
if not elem_uuid:
|
||||||
elem_uuid = _sg_root_uuid(d + ".xml")
|
elem_uuid = _sg_root_uuid(d + ".xml")
|
||||||
if not cfg_dir:
|
if not cfg_dir:
|
||||||
@@ -209,7 +226,9 @@ def local_name(node):
|
|||||||
# ── helpers ──────────────────────────────────────────────────
|
# ── helpers ──────────────────────────────────────────────────
|
||||||
|
|
||||||
def esc_xml(s):
|
def esc_xml(s):
|
||||||
return s.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"')
|
"""Экранирование ТЕКСТА элемента: только & < > . Кавычки платформа в тексте не экранирует
|
||||||
|
(92142 сырых кавычки на корпус, ни одной "); " она принимает, но нормализует обратно."""
|
||||||
|
return s.replace('&', '&').replace('<', '<').replace('>', '>')
|
||||||
|
|
||||||
|
|
||||||
# ── 1. Load Form.xml ────────────────────────────────────────
|
# ── 1. Load Form.xml ────────────────────────────────────────
|
||||||
@@ -1458,14 +1477,40 @@ if elem_events_list:
|
|||||||
|
|
||||||
# ── 13. Save ────────────────────────────────────────────────
|
# ── 13. Save ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Round-trip: определить стиль исходного файла (на диске он ещё не перезаписан).
|
||||||
|
try:
|
||||||
|
_fe_raw = open(resolved_form_path, "rb").read()
|
||||||
|
except OSError:
|
||||||
|
_fe_raw = None
|
||||||
|
if _fe_raw is not None:
|
||||||
|
_fe_bom = _fe_raw.startswith(b"\xef\xbb\xbf")
|
||||||
|
_fe_body = _fe_raw[3:] if _fe_bom else _fe_raw
|
||||||
|
_fe_crlf = b"\r\n" in _fe_body
|
||||||
|
_fe_enc_m = re.search(rb'encoding="([^"]+)"', _fe_body[:200])
|
||||||
|
_fe_enc = _fe_enc_m.group(1).decode("ascii") if _fe_enc_m else "utf-8"
|
||||||
|
_fe_final_nl = _fe_body.endswith(b"\n")
|
||||||
|
else:
|
||||||
|
_fe_bom, _fe_crlf, _fe_enc, _fe_final_nl = True, False, "utf-8", True
|
||||||
|
|
||||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
||||||
# Fix XML declaration quotes
|
# Восстановить регистр encoding как в оригинале.
|
||||||
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
xml_bytes = xml_bytes.replace(
|
||||||
if not xml_bytes.endswith(b"\n"):
|
b"<?xml version='1.0' encoding='UTF-8'?>",
|
||||||
|
b'<?xml version="1.0" encoding="' + _fe_enc.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"))
|
||||||
|
# Финальный перенос — как в оригинале.
|
||||||
|
xml_bytes = xml_bytes.rstrip(b"\n")
|
||||||
|
if _fe_final_nl:
|
||||||
xml_bytes += b"\n"
|
xml_bytes += b"\n"
|
||||||
# Write with BOM
|
# EOL — как в оригинале.
|
||||||
|
if _fe_crlf:
|
||||||
|
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
|
||||||
|
# Write preserving BOM as in original.
|
||||||
with open(resolved_form_path, "wb") as f:
|
with open(resolved_form_path, "wb") as f:
|
||||||
f.write(b'\xef\xbb\xbf')
|
if _fe_bom:
|
||||||
|
f.write(b'\xef\xbb\xbf')
|
||||||
f.write(xml_bytes)
|
f.write(xml_bytes)
|
||||||
|
|
||||||
# ── 14. Summary ─────────────────────────────────────────────
|
# ── 14. Summary ─────────────────────────────────────────────
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# form-info v1.4 — Analyze 1C managed form structure
|
# form-info v1.5 — Analyze 1C managed form structure
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory=$true)]
|
[Parameter(Mandatory=$true)]
|
||||||
@@ -372,6 +372,16 @@ if ($formsIdx -ge 0 -and ($formsIdx + 1) -lt $parts.Count) {
|
|||||||
# See docs/1c-support-state-spec.md. Walks up from the target path, taking the
|
# See docs/1c-support-state-spec.md. Walks up from the target path, taking the
|
||||||
# uuid of the nearest element meta-xml (form/template/etc.) and the config root
|
# uuid of the nearest element meta-xml (form/template/etc.) and the config root
|
||||||
# bin. Never throws — degrades to "не на поддержке".
|
# bin. Never throws — degrades to "не на поддержке".
|
||||||
|
function Test-ExternalObjectRoot([string]$xmlPath) {
|
||||||
|
if (-not (Test-Path $xmlPath)) { return $false }
|
||||||
|
try {
|
||||||
|
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||||
|
$el = $mx.DocumentElement.FirstChild
|
||||||
|
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||||
|
if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
|
||||||
|
} catch {}
|
||||||
|
return $false
|
||||||
|
}
|
||||||
function Get-SupportStatusForPath([string]$targetPath) {
|
function Get-SupportStatusForPath([string]$targetPath) {
|
||||||
try {
|
try {
|
||||||
$rp = (Resolve-Path $targetPath).Path
|
$rp = (Resolve-Path $targetPath).Path
|
||||||
@@ -390,8 +400,10 @@ function Get-SupportStatusForPath([string]$targetPath) {
|
|||||||
}
|
}
|
||||||
# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
|
# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
|
||||||
$elemUuid = Get-RootUuid $rp
|
$elemUuid = Get-RootUuid $rp
|
||||||
|
if (Test-ExternalObjectRoot $rp) { return $null }
|
||||||
$d = [System.IO.Path]::GetDirectoryName($rp)
|
$d = [System.IO.Path]::GetDirectoryName($rp)
|
||||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||||
|
if (Test-ExternalObjectRoot "$d.xml") { return $null }
|
||||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||||
if (-not $binPath) {
|
if (-not $binPath) {
|
||||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||||
@@ -448,7 +460,8 @@ if ($formTitle) { $header += " — `"$formTitle`"" }
|
|||||||
if ($objectContext) { $header += " ($objectContext)" }
|
if ($objectContext) { $header += " ($objectContext)" }
|
||||||
$header += " ==="
|
$header += " ==="
|
||||||
$lines += $header
|
$lines += $header
|
||||||
$lines += "Поддержка: $(Get-SupportStatusForPath $FormPath)"
|
$support = Get-SupportStatusForPath $FormPath
|
||||||
|
if ($null -ne $support) { $lines += "Поддержка: $support" }
|
||||||
|
|
||||||
# --- Form properties (Title excluded — shown in header) ---
|
# --- Form properties (Title excluded — shown in header) ---
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# form-info v1.4 — Analyze 1C managed form structure
|
# form-info v1.5 — Analyze 1C managed form structure
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -353,14 +353,29 @@ def get_support_status_for_path(target_path):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return None
|
return None
|
||||||
|
def is_external_root(xml_path):
|
||||||
|
if not os.path.isfile(xml_path):
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
mx = etree.parse(xml_path).getroot()
|
||||||
|
for child in mx:
|
||||||
|
if isinstance(child.tag, str):
|
||||||
|
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
return False
|
||||||
rp = os.path.abspath(target_path)
|
rp = os.path.abspath(target_path)
|
||||||
# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
|
# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
|
||||||
elem_uuid = root_uuid(rp)
|
elem_uuid = root_uuid(rp)
|
||||||
|
if is_external_root(rp):
|
||||||
|
return None
|
||||||
bin_path = None
|
bin_path = None
|
||||||
d = os.path.dirname(rp)
|
d = os.path.dirname(rp)
|
||||||
for _ in range(12):
|
for _ in range(12):
|
||||||
if not d:
|
if not d:
|
||||||
break
|
break
|
||||||
|
if is_external_root(d + ".xml"):
|
||||||
|
return None
|
||||||
if not elem_uuid:
|
if not elem_uuid:
|
||||||
elem_uuid = root_uuid(d + ".xml")
|
elem_uuid = root_uuid(d + ".xml")
|
||||||
if not bin_path:
|
if not bin_path:
|
||||||
@@ -513,7 +528,9 @@ def main():
|
|||||||
header += f" ({object_context})"
|
header += f" ({object_context})"
|
||||||
header += " ==="
|
header += " ==="
|
||||||
lines.append(header)
|
lines.append(header)
|
||||||
lines.append(f"Поддержка: {get_support_status_for_path(form_path)}")
|
_support = get_support_status_for_path(form_path)
|
||||||
|
if _support is not None:
|
||||||
|
lines.append(f"Поддержка: {_support}")
|
||||||
|
|
||||||
# --- Form properties (Title excluded -- shown in header) ---
|
# --- Form properties (Title excluded -- shown in header) ---
|
||||||
prop_names = [
|
prop_names = [
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# form-remove v1.2 — Remove form from 1C object
|
# form-remove v1.4 — Remove form from 1C object
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -68,10 +68,14 @@ foreach ($node in $formNodes) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Очистить DefaultForm если указывала на эту форму
|
# Очистить любые Default*/Auxiliary* form-слоты, указывавшие на удалённую форму
|
||||||
$defaultForm = $xmlDoc.SelectSingleNode("//md:DefaultForm", $nsMgr)
|
# (form-add пишет свойство по назначению: DefaultObjectForm/DefaultListForm/
|
||||||
if ($defaultForm -and $defaultForm.InnerText -match "Form\.$FormName$") {
|
# DefaultChoiceForm/DefaultRecordForm/DefaultForm — не только generic DefaultForm).
|
||||||
$defaultForm.InnerText = ""
|
$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) {
|
||||||
|
$node.InnerText = ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Сохранить с BOM
|
# Сохранить с BOM
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# remove-form v1.1 — Remove form from 1C object
|
# remove-form v1.4 — Remove form from 1C object
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -13,14 +13,50 @@ from lxml import etree
|
|||||||
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
|
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
|
||||||
|
|
||||||
|
|
||||||
def save_xml_with_bom(tree, path):
|
def _detect_xml_style(path):
|
||||||
"""Save XML tree to file with UTF-8 BOM."""
|
"""Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
|
||||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
финальный перенос. None → файл новый (сохранить текущее поведение)."""
|
||||||
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
try:
|
||||||
if not xml_bytes.endswith(b"\n"):
|
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)."""
|
||||||
|
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"))
|
||||||
|
# Финальный перенос — как в оригинале (новый файл → есть)
|
||||||
|
want_final_nl = style["final_nl"] if style else True
|
||||||
|
xml_bytes = xml_bytes.rstrip(b"\n")
|
||||||
|
if want_final_nl:
|
||||||
xml_bytes += b"\n"
|
xml_bytes += b"\n"
|
||||||
|
# EOL — как в оригинале (новый файл → LF, текущее поведение)
|
||||||
|
if style and style["crlf"]:
|
||||||
|
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:
|
with open(path, "wb") as f:
|
||||||
f.write(b"\xef\xbb\xbf")
|
if style is None or style["bom"]:
|
||||||
|
f.write(b"\xef\xbb\xbf")
|
||||||
f.write(xml_bytes)
|
f.write(xml_bytes)
|
||||||
|
|
||||||
|
|
||||||
@@ -85,11 +121,15 @@ def main():
|
|||||||
parent.remove(node)
|
parent.remove(node)
|
||||||
break
|
break
|
||||||
|
|
||||||
# Clear DefaultForm if it pointed to removed form
|
# Clear any Default*/Auxiliary* form slot that pointed to the removed form
|
||||||
default_form = root.find(".//md:DefaultForm", NSMAP)
|
# (form-add writes the purpose-specific property: DefaultObjectForm / DefaultListForm /
|
||||||
if default_form is not None and default_form.text:
|
# DefaultChoiceForm / DefaultRecordForm / DefaultForm — not just generic DefaultForm).
|
||||||
if re.search(rf"Form\.{re.escape(form_name)}$", default_form.text):
|
ref_re = re.compile(rf"Form\.{re.escape(form_name)}$")
|
||||||
default_form.text = ""
|
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):
|
||||||
|
el.text = ""
|
||||||
|
|
||||||
# Save with BOM
|
# Save with BOM
|
||||||
save_xml_with_bom(tree, root_xml_full)
|
save_xml_with_bom(tree, root_xml_full)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# form-validate v1.8 — Validate 1C managed form
|
# form-validate v1.9 — Validate 1C managed form
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -127,10 +127,11 @@ if ($root.LocalName -ne "Form") {
|
|||||||
Report-Error "Root element is '$($root.LocalName)', expected 'Form'"
|
Report-Error "Root element is '$($root.LocalName)', expected 'Form'"
|
||||||
} else {
|
} else {
|
||||||
$version = $root.GetAttribute("version")
|
$version = $root.GetAttribute("version")
|
||||||
if ($version -eq "2.17" -or $version -eq "2.20") {
|
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27).
|
||||||
|
if ($version -in @("2.17", "2.18", "2.19", "2.20")) {
|
||||||
Report-OK "Root element: Form version=$version"
|
Report-OK "Root element: Form version=$version"
|
||||||
} elseif ($version) {
|
} elseif ($version) {
|
||||||
Report-Warn "Form version='$version' (expected 2.17 or 2.20)"
|
Report-Warn "Form version='$version' (expected 2.17-2.20)"
|
||||||
} else {
|
} else {
|
||||||
Report-Warn "Form version attribute missing"
|
Report-Warn "Form version attribute missing"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# form-validate v1.8 — Validate 1C managed form
|
# form-validate v1.9 — Validate 1C managed form
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -161,10 +161,11 @@ def main():
|
|||||||
report_error(f"Root element is '{localname(root)}', expected 'Form'")
|
report_error(f"Root element is '{localname(root)}', expected 'Form'")
|
||||||
else:
|
else:
|
||||||
version = root.get("version", "")
|
version = root.get("version", "")
|
||||||
if version in ("2.17", "2.20"):
|
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27).
|
||||||
|
if version in ("2.17", "2.18", "2.19", "2.20"):
|
||||||
report_ok(f"Root element: Form version={version}")
|
report_ok(f"Root element: Form version={version}")
|
||||||
elif version:
|
elif version:
|
||||||
report_warn(f"Form version='{version}' (expected 2.17 or 2.20)")
|
report_warn(f"Form version='{version}' (expected 2.17-2.20)")
|
||||||
else:
|
else:
|
||||||
report_warn("Form version attribute missing")
|
report_warn("Form version attribute missing")
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# help-add v1.7 — Add built-in help to 1C object
|
# help-add v1.9 — Add built-in help to 1C object
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -28,6 +28,16 @@ function Get-RootUuid([string]$xmlPath) {
|
|||||||
} catch {}
|
} catch {}
|
||||||
return $null
|
return $null
|
||||||
}
|
}
|
||||||
|
function Test-ExternalObjectRoot([string]$xmlPath) {
|
||||||
|
if (-not (Test-Path $xmlPath)) { return $false }
|
||||||
|
try {
|
||||||
|
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||||
|
$el = $mx.DocumentElement.FirstChild
|
||||||
|
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||||
|
if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
|
||||||
|
} catch {}
|
||||||
|
return $false
|
||||||
|
}
|
||||||
function Find-V8Project([string]$startDir) {
|
function Find-V8Project([string]$startDir) {
|
||||||
$d = $startDir
|
$d = $startDir
|
||||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||||
@@ -64,10 +74,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
|||||||
try {
|
try {
|
||||||
$rp = $targetPath
|
$rp = $targetPath
|
||||||
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
||||||
|
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||||
|
if (Test-ExternalObjectRoot $rp) { return }
|
||||||
$elemUuid = Get-RootUuid $rp
|
$elemUuid = Get-RootUuid $rp
|
||||||
$cfgDir = $null; $binPath = $null
|
$cfgDir = $null; $binPath = $null
|
||||||
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
||||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||||
|
if (Test-ExternalObjectRoot "$d.xml") { return }
|
||||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||||
if (-not $cfgDir) {
|
if (-not $cfgDir) {
|
||||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# add-help v1.7 — Add built-in help to 1C object
|
# add-help v1.9 — Add built-in help to 1C object
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -33,6 +33,18 @@ def _sg_root_uuid(xml_path):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _sg_is_external_root(xml_path):
|
||||||
|
if not os.path.isfile(xml_path):
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
mx = etree.parse(xml_path).getroot()
|
||||||
|
for child in mx:
|
||||||
|
if isinstance(child.tag, str):
|
||||||
|
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
return False
|
||||||
|
|
||||||
def _sg_find_v8project(start_dir):
|
def _sg_find_v8project(start_dir):
|
||||||
d = start_dir
|
d = start_dir
|
||||||
for _ in range(20):
|
for _ in range(20):
|
||||||
@@ -72,6 +84,9 @@ def _sg_get_edit_mode(cfg_dir):
|
|||||||
def assert_edit_allowed(target_path, require):
|
def assert_edit_allowed(target_path, require):
|
||||||
try:
|
try:
|
||||||
rp = os.path.abspath(target_path)
|
rp = os.path.abspath(target_path)
|
||||||
|
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||||
|
if _sg_is_external_root(rp):
|
||||||
|
return
|
||||||
elem_uuid = _sg_root_uuid(rp)
|
elem_uuid = _sg_root_uuid(rp)
|
||||||
cfg_dir = None
|
cfg_dir = None
|
||||||
bin_path = None
|
bin_path = None
|
||||||
@@ -79,6 +94,8 @@ def assert_edit_allowed(target_path, require):
|
|||||||
for _ in range(12):
|
for _ in range(12):
|
||||||
if not d:
|
if not d:
|
||||||
break
|
break
|
||||||
|
if _sg_is_external_root(d + ".xml"):
|
||||||
|
return
|
||||||
if not elem_uuid:
|
if not elem_uuid:
|
||||||
elem_uuid = _sg_root_uuid(d + ".xml")
|
elem_uuid = _sg_root_uuid(d + ".xml")
|
||||||
if not cfg_dir:
|
if not cfg_dir:
|
||||||
@@ -188,14 +205,50 @@ def detect_format_version(d):
|
|||||||
return "2.17"
|
return "2.17"
|
||||||
|
|
||||||
|
|
||||||
def save_xml_with_bom(tree, path):
|
def _detect_xml_style(path):
|
||||||
"""Save XML tree to file with UTF-8 BOM."""
|
"""Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
|
||||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
финальный перенос. None → файл новый (сохранить текущее поведение)."""
|
||||||
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
try:
|
||||||
if not xml_bytes.endswith(b"\n"):
|
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)."""
|
||||||
|
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"))
|
||||||
|
# Финальный перенос — как в оригинале (новый файл → есть)
|
||||||
|
want_final_nl = style["final_nl"] if style else True
|
||||||
|
xml_bytes = xml_bytes.rstrip(b"\n")
|
||||||
|
if want_final_nl:
|
||||||
xml_bytes += b"\n"
|
xml_bytes += b"\n"
|
||||||
|
# EOL — как в оригинале (новый файл → LF, текущее поведение)
|
||||||
|
if style and style["crlf"]:
|
||||||
|
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:
|
with open(path, "wb") as f:
|
||||||
f.write(b"\xef\xbb\xbf")
|
if style is None or style["bom"]:
|
||||||
|
f.write(b"\xef\xbb\xbf")
|
||||||
f.write(xml_bytes)
|
f.write(xml_bytes)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# img-grid v1.1 — Overlay numbered grid on image
|
||||||
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
"""Overlay a numbered grid on an image to help determine column/row proportions.
|
"""Overlay a numbered grid on an image to help determine column/row proportions.
|
||||||
|
|
||||||
Usage: python overlay-grid.py <image> [-c COLS] [-r ROWS] [-o OUTPUT]
|
Usage: python overlay-grid.py <image> [-c COLS] [-r ROWS] [-o OUTPUT]
|
||||||
@@ -29,6 +32,11 @@ def main():
|
|||||||
parser.add_argument("-o", "--output", help="Output path (default: <name>-grid.<ext>)")
|
parser.add_argument("-o", "--output", help="Output path (default: <name>-grid.<ext>)")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.cols <= 0:
|
||||||
|
parser.error("--cols must be greater than 0")
|
||||||
|
if args.rows < 0:
|
||||||
|
parser.error("--rows must be greater than or equal to 0")
|
||||||
|
|
||||||
src = Image.open(args.image).convert("RGBA")
|
src = Image.open(args.image).convert("RGBA")
|
||||||
sw, sh = src.size
|
sw, sh = src.size
|
||||||
|
|
||||||
@@ -36,7 +44,7 @@ def main():
|
|||||||
step_x = sw / cols
|
step_x = sw / cols
|
||||||
rows = args.rows
|
rows = args.rows
|
||||||
if rows == 0:
|
if rows == 0:
|
||||||
rows = round(sh / step_x)
|
rows = max(1, round(sh / step_x))
|
||||||
step_y = sh / rows
|
step_y = sh / rows
|
||||||
|
|
||||||
# Canvas with margins for labels
|
# Canvas with margins for labels
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# interface-edit v1.6 — Edit 1C CommandInterface.xml
|
# interface-edit v1.9 — Edit 1C CommandInterface.xml
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)][Alias('Path')][string]$CIPath,
|
[Parameter(Mandatory)][Alias('Path')][string]$CIPath,
|
||||||
@@ -39,6 +39,16 @@ function Get-RootUuid([string]$xmlPath) {
|
|||||||
} catch {}
|
} catch {}
|
||||||
return $null
|
return $null
|
||||||
}
|
}
|
||||||
|
function Test-ExternalObjectRoot([string]$xmlPath) {
|
||||||
|
if (-not (Test-Path $xmlPath)) { return $false }
|
||||||
|
try {
|
||||||
|
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||||
|
$el = $mx.DocumentElement.FirstChild
|
||||||
|
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||||
|
if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
|
||||||
|
} catch {}
|
||||||
|
return $false
|
||||||
|
}
|
||||||
function Find-V8Project([string]$startDir) {
|
function Find-V8Project([string]$startDir) {
|
||||||
$d = $startDir
|
$d = $startDir
|
||||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||||
@@ -75,10 +85,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
|||||||
try {
|
try {
|
||||||
$rp = $targetPath
|
$rp = $targetPath
|
||||||
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
||||||
|
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||||
|
if (Test-ExternalObjectRoot $rp) { return }
|
||||||
$elemUuid = Get-RootUuid $rp
|
$elemUuid = Get-RootUuid $rp
|
||||||
$cfgDir = $null; $binPath = $null
|
$cfgDir = $null; $binPath = $null
|
||||||
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
||||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||||
|
if (Test-ExternalObjectRoot "$d.xml") { return }
|
||||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||||
if (-not $cfgDir) {
|
if (-not $cfgDir) {
|
||||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||||
@@ -151,7 +164,10 @@ function Detect-FormatVersion([string]$dir) {
|
|||||||
while ($d) {
|
while ($d) {
|
||||||
$cfgPath = Join-Path $d "Configuration.xml"
|
$cfgPath = Join-Path $d "Configuration.xml"
|
||||||
if (Test-Path $cfgPath) {
|
if (Test-Path $cfgPath) {
|
||||||
$head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length))
|
$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] }
|
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
||||||
}
|
}
|
||||||
$parent = Split-Path $d -Parent
|
$parent = Split-Path $d -Parent
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# interface-edit v1.6 — Edit 1C CommandInterface.xml
|
# interface-edit v1.9 — Edit 1C CommandInterface.xml
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -31,6 +31,18 @@ def _sg_root_uuid(xml_path):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _sg_is_external_root(xml_path):
|
||||||
|
if not os.path.isfile(xml_path):
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
mx = etree.parse(xml_path).getroot()
|
||||||
|
for child in mx:
|
||||||
|
if isinstance(child.tag, str):
|
||||||
|
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
return False
|
||||||
|
|
||||||
def _sg_find_v8project(start_dir):
|
def _sg_find_v8project(start_dir):
|
||||||
d = start_dir
|
d = start_dir
|
||||||
for _ in range(20):
|
for _ in range(20):
|
||||||
@@ -70,6 +82,9 @@ def _sg_get_edit_mode(cfg_dir):
|
|||||||
def assert_edit_allowed(target_path, require):
|
def assert_edit_allowed(target_path, require):
|
||||||
try:
|
try:
|
||||||
rp = os.path.abspath(target_path)
|
rp = os.path.abspath(target_path)
|
||||||
|
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||||
|
if _sg_is_external_root(rp):
|
||||||
|
return
|
||||||
elem_uuid = _sg_root_uuid(rp)
|
elem_uuid = _sg_root_uuid(rp)
|
||||||
cfg_dir = None
|
cfg_dir = None
|
||||||
bin_path = None
|
bin_path = None
|
||||||
@@ -77,6 +92,8 @@ def assert_edit_allowed(target_path, require):
|
|||||||
for _ in range(12):
|
for _ in range(12):
|
||||||
if not d:
|
if not d:
|
||||||
break
|
break
|
||||||
|
if _sg_is_external_root(d + ".xml"):
|
||||||
|
return
|
||||||
if not elem_uuid:
|
if not elem_uuid:
|
||||||
elem_uuid = _sg_root_uuid(d + ".xml")
|
elem_uuid = _sg_root_uuid(d + ".xml")
|
||||||
if not cfg_dir:
|
if not cfg_dir:
|
||||||
@@ -270,13 +287,49 @@ def parse_value_list(val):
|
|||||||
return [val]
|
return [val]
|
||||||
|
|
||||||
|
|
||||||
def save_xml_bom(tree, path):
|
def _detect_xml_style(path):
|
||||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
"""Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
|
||||||
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
финальный перенос. None → файл новый (сохранить текущее поведение)."""
|
||||||
if not xml_bytes.endswith(b"\n"):
|
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)."""
|
||||||
|
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"))
|
||||||
|
# Финальный перенос — как в оригинале (новый файл → есть)
|
||||||
|
want_final_nl = style["final_nl"] if style else True
|
||||||
|
xml_bytes = xml_bytes.rstrip(b"\n")
|
||||||
|
if want_final_nl:
|
||||||
xml_bytes += b"\n"
|
xml_bytes += b"\n"
|
||||||
|
# EOL — как в оригинале (новый файл → LF, текущее поведение)
|
||||||
|
if style and style["crlf"]:
|
||||||
|
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
|
||||||
|
return xml_bytes
|
||||||
|
|
||||||
|
|
||||||
|
def save_xml_bom(tree, path):
|
||||||
|
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:
|
with open(path, "wb") as f:
|
||||||
f.write(b"\xef\xbb\xbf")
|
if style is None or style["bom"]:
|
||||||
|
f.write(b"\xef\xbb\xbf")
|
||||||
f.write(xml_bytes)
|
f.write(xml_bytes)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,18 +9,19 @@ allowed-tools:
|
|||||||
- Glob
|
- Glob
|
||||||
---
|
---
|
||||||
|
|
||||||
# /meta-compile — генерация объектов метаданных из JSON DSL
|
# /meta-compile — генерация объектов метаданных из JSON
|
||||||
|
|
||||||
Принимает JSON-определение объекта метаданных → генерирует XML + модули в структуре выгрузки конфигурации + регистрирует в Configuration.xml.
|
Принимает JSON-определение объекта → генерирует XML + модули в структуре выгрузки конфигурации и
|
||||||
|
регистрирует объект в `Configuration.xml`.
|
||||||
|
|
||||||
|
`ConfigDumpInfo.xml` намеренно не трогается: это служебный файл версий объектов, которым управляет
|
||||||
|
платформа (для инкрементальной выгрузки).
|
||||||
|
|
||||||
## Порядок работы
|
## Порядок работы
|
||||||
|
|
||||||
1. Составь JSON по синтаксису и примерам ниже → запиши во временный файл
|
1. Составь JSON по синтаксису ниже → запиши во временный файл.
|
||||||
2. Запусти скрипт meta-compile
|
2. Запусти скрипт.
|
||||||
3. Если нужно изменить созданный объект — `/meta-edit`
|
3. Изменить созданный объект — `/meta-edit`; проверить — `/meta-validate`.
|
||||||
4. Если нужно проверить — `/meta-validate`
|
|
||||||
|
|
||||||
## Команда
|
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-compile.ps1" -JsonPath "<json>" -OutputDir "<ConfigDir>"
|
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-compile.ps1" -JsonPath "<json>" -OutputDir "<ConfigDir>"
|
||||||
@@ -28,92 +29,110 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-compile.ps1" -
|
|||||||
|
|
||||||
| Параметр | Описание |
|
| Параметр | Описание |
|
||||||
|----------|----------|
|
|----------|----------|
|
||||||
| `JsonPath` | Путь к JSON-файлу (один объект `{...}` или массив `[{...}, ...]`) |
|
| `JsonPath` | Путь к JSON-файлу |
|
||||||
| `OutputDir` | Корень выгрузки конфигурации (где `Configuration.xml`, `Catalogs/`, `Documents/` и т.д.) |
|
| `OutputDir` | Корень выгрузки конфигурации (где `Configuration.xml`, `Catalogs/`, `Documents/`, …) |
|
||||||
|
|
||||||
## JSON DSL
|
## Формат JSON
|
||||||
|
|
||||||
### Общая структура
|
**Один объект** `{ ... }` или **массив** объектов `[{ ... }, { ... }]` (batch — несколько объектов за прогон).
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "type": "Catalog", "name": "Номенклатура", ...свойства типа... }
|
{ "type": "Catalog", "name": "Номенклатура", "...свойства типа...": "..." }
|
||||||
```
|
```
|
||||||
|
|
||||||
`type` и `name` — обязательные. `synonym` генерируется из `name` автоматически (CamelCase → слова через пробел). Можно задать явно: `"synonym": "Мой синоним"`.
|
`type` и `name` — обязательные. Остальное — по типу (см. индекс ниже). `synonym` по умолчанию выводится из
|
||||||
|
`name` (CamelCase → слова через пробел); можно задать явно строкой или мультиязычно: `"synonym": { "ru": "…", "en": "…" }`.
|
||||||
|
|
||||||
### Shorthand реквизитов
|
## Реквизиты (shorthand)
|
||||||
|
|
||||||
Используется в `attributes`, `dimensions`, `resources`, `tabularSections`:
|
Массивы `attributes`, `dimensions`, `resources` и колонки в `tabularSections` задаются строками:
|
||||||
|
|
||||||
```
|
```
|
||||||
"ИмяРеквизита" → String(10) по умолчанию
|
"Имя" → String(10)
|
||||||
"ИмяРеквизита: Тип" → с типом
|
"Имя: Тип" → с типом
|
||||||
"ИмяРеквизита: Тип | req, index" → с флагами
|
"Имя: Тип | req, index" → с флагами
|
||||||
```
|
```
|
||||||
|
|
||||||
Типы: `String(100)`, `Number(15,2)`, `Boolean`, `Date`, `DateTime`, `CatalogRef.Xxx`, `DocumentRef.Xxx`, `EnumRef.Xxx`, `DefinedType.Xxx` и др. ссылочные.
|
**Типы:** `String(100)`, `String(10, fixed)` (фикс. длина), `Number(15,2)`, `Boolean`, `Date`, `DateTime`,
|
||||||
|
`Time`, ссылочные `CatalogRef.Xxx` / `DocumentRef.Xxx` / `EnumRef.Xxx` / `DefinedType.Xxx` и т.п.
|
||||||
|
Составной тип — через `+`: `"Значение: String + Number(15,2) + CatalogRef.Контрагенты"`.
|
||||||
|
|
||||||
Составной тип: `"Значение: String + Number(15,2) + CatalogRef.Контрагенты"`.
|
**Флаги** (после `|`, через запятую):
|
||||||
|
|
||||||
Флаги: `req`, `index`, `indexAdditional`, `nonneg`, `master`, `mainFilter`, `denyIncomplete`, `useInTotals`.
|
| Флаг | Значение | Где |
|
||||||
|
|------|----------|-----|
|
||||||
|
| `req` | обязательное заполнение | attributes, dimensions, resources |
|
||||||
|
| `index` | индексировать | attributes, dimensions |
|
||||||
|
| `indexAdditional` | индекс с доп. упорядочиванием | attributes |
|
||||||
|
| `multiline` | многострочное поле | attributes |
|
||||||
|
| `nonneg` | неотрицательное (Number) | attributes, resources |
|
||||||
|
| `master` | ведущее измерение | dimensions (регистры) |
|
||||||
|
| `mainFilter` | основной отбор | dimensions (регистры) |
|
||||||
|
| `denyIncomplete` | запрет незаполненных | dimensions |
|
||||||
|
| `useInTotals` | использовать в итогах | dimensions (регистр накопления) |
|
||||||
|
|
||||||
### Свойства по типам
|
Реквизиту нужны свойства сверх shorthand (значение заполнения, параметры выбора, формат, подсказка, …) —
|
||||||
|
задаётся **объектной формой**, см. `reference/attributes.md`.
|
||||||
|
|
||||||
Примеров и shorthand-синтаксиса выше достаточно для типовых задач. Если нужны свойства типа, не показанные в примерах, и их допустимые значения — см. reference-файл:
|
## Табличные части
|
||||||
|
|
||||||
- `reference/types-basic.md` — Catalog, Document, Enum, Constant, DefinedType, Report, DataProcessor
|
|
||||||
- `reference/types-registers.md` — InformationRegister, AccumulationRegister, AccountingRegister, CalculationRegister, ChartOfAccounts, ChartOfCharacteristicTypes, ChartOfCalculationTypes
|
|
||||||
- `reference/types-process.md` — BusinessProcess, Task, ExchangePlan, CommonModule, ScheduledJob, EventSubscription, DocumentJournal
|
|
||||||
- `reference/types-web.md` — HTTPService, WebService
|
|
||||||
|
|
||||||
Эта инструкция и reference-файлы — полная документация для генерации. Не ищи примеры XML в выгрузках конфигураций.
|
|
||||||
|
|
||||||
## Примеры паттернов DSL
|
|
||||||
|
|
||||||
### Минимальный объект
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "type": "Catalog", "name": "Валюты" }
|
"tabularSections": { "Товары": ["Номенклатура: CatalogRef.Номенклатура", "Количество: Number(15,3)"] }
|
||||||
```
|
```
|
||||||
|
|
||||||
### С реквизитами
|
Ключ — имя ТЧ, значение — массив колонок (shorthand) ЛИБО объект со свойствами ТЧ (см. `reference/attributes.md`).
|
||||||
|
|
||||||
|
## Индекс: свойства по типам
|
||||||
|
|
||||||
|
Для каждого типа — свой reference-файл со свойствами, дефолтами и допустимыми значениями:
|
||||||
|
|
||||||
|
| Тип(ы) | Файл |
|
||||||
|
|--------|------|
|
||||||
|
| Catalog (справочник) | `reference/catalog.md` |
|
||||||
|
| Document, DocumentJournal, Sequence, DocumentNumerator | `reference/document.md` |
|
||||||
|
| InformationRegister, AccumulationRegister, AccountingRegister, CalculationRegister | `reference/registers.md` |
|
||||||
|
| ChartOfAccounts, ChartOfCharacteristicTypes, ChartOfCalculationTypes | `reference/charts.md` |
|
||||||
|
| ExchangePlan | `reference/exchangeplan.md` |
|
||||||
|
| BusinessProcess, Task | `reference/process.md` |
|
||||||
|
| Report, DataProcessor | `reference/report-dataprocessor.md` |
|
||||||
|
| CommonModule, ScheduledJob, EventSubscription | `reference/code.md` |
|
||||||
|
| HTTPService, WebService | `reference/web.md` |
|
||||||
|
| Enum, Constant, DefinedType | `reference/simple.md` |
|
||||||
|
| FunctionalOption, FilterCriterion, SettingsStorage, CommonForm, CommonPicture, CommonTemplate, служебные | `reference/other-types.md` |
|
||||||
|
|
||||||
|
Кросс-типовые детали:
|
||||||
|
- **`reference/attributes.md`** — объектная форма реквизита и колонки ТЧ (значение заполнения, параметры
|
||||||
|
выбора, формат, подсказка, границы, …) + свойства самой ТЧ.
|
||||||
|
- **`reference/blocks.md`** — блоки объекта: представления, команды (+ характеристики/стандартные реквизиты).
|
||||||
|
|
||||||
|
Эта инструкция и reference-файлы — полная документация. Не ищи примеры XML в выгрузках конфигураций.
|
||||||
|
|
||||||
|
## Примеры
|
||||||
|
|
||||||
|
Справочник с реквизитами:
|
||||||
```json
|
```json
|
||||||
{
|
{ "type": "Catalog", "name": "Организации", "descriptionLength": 100,
|
||||||
"type": "Catalog", "name": "Организации",
|
"attributes": ["ИНН: String(12)", "КПП: String(9)", "Директор: CatalogRef.ФизическиеЛица"] }
|
||||||
"descriptionLength": 100,
|
|
||||||
"attributes": ["ИНН: String(12)", "КПП: String(9)", "Директор: CatalogRef.ФизическиеЛица"]
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### С табличной частью
|
Документ с движениями и ТЧ:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{ "type": "Document", "name": "ПриходнаяНакладная",
|
||||||
"type": "Document", "name": "ПриходнаяНакладная",
|
|
||||||
"registerRecords": ["AccumulationRegister.ОстаткиТоваров"],
|
"registerRecords": ["AccumulationRegister.ОстаткиТоваров"],
|
||||||
"attributes": ["Организация: CatalogRef.Организации", "Контрагент: CatalogRef.Контрагенты"],
|
"attributes": ["Организация: CatalogRef.Организации", "Контрагент: CatalogRef.Контрагенты"],
|
||||||
"tabularSections": { "Товары": ["Номенклатура: CatalogRef.Номенклатура", "Количество: Number(15,3)", "Цена: Number(15,2)"] }
|
"tabularSections": { "Товары": ["Номенклатура: CatalogRef.Номенклатура", "Количество: Number(15,3)", "Цена: Number(15,2)"] } }
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Регистровый паттерн (измерения + ресурсы)
|
Регистр сведений:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{ "type": "InformationRegister", "name": "КурсыВалют", "periodicity": "Day",
|
||||||
"type": "InformationRegister", "name": "КурсыВалют", "periodicity": "Day",
|
|
||||||
"dimensions": ["Валюта: CatalogRef.Валюты | master, mainFilter, denyIncomplete"],
|
"dimensions": ["Валюта: CatalogRef.Валюты | master, mainFilter, denyIncomplete"],
|
||||||
"resources": ["Курс: Number(15,4)", "Кратность: Number(10,0)"]
|
"resources": ["Курс: Number(15,4)", "Кратность: Number(10,0)"] }
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Batch — несколько объектов в одном файле
|
Batch:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
[
|
[ { "type": "Enum", "name": "Статусы", "values": ["Новый", "Закрыт"] },
|
||||||
{ "type": "Enum", "name": "Статусы", "values": ["Новый", "Закрыт"] },
|
|
||||||
{ "type": "Catalog", "name": "Валюты" },
|
{ "type": "Catalog", "name": "Валюты" },
|
||||||
{ "type": "Constant", "name": "ОсновнаяВалюта", "valueType": "CatalogRef.Валюты" }
|
{ "type": "Constant", "name": "ОсновнаяВалюта", "valueType": "CatalogRef.Валюты" } ]
|
||||||
]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
# Объектная форма реквизита и табличной части
|
||||||
|
|
||||||
|
Когда реквизиту (в `attributes` / `dimensions` / `resources` / колонках ТЧ) нужны свойства сверх
|
||||||
|
shorthand — вместо строки задаётся объект:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "name": "Цена", "type": "Number(15,2)", "tooltip": "Цена за единицу", "fillValue": 0 }
|
||||||
|
```
|
||||||
|
|
||||||
|
`name` и `type` обязательны (тип можно задать и раздельно: `"type": "Number", "length": 15, "precision": 2`).
|
||||||
|
Остальные ключи — ниже, все со значением по умолчанию (не задавать, если устраивает дефолт).
|
||||||
|
|
||||||
|
## Свойства реквизита
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `synonym` | из имени | ML (строка или `{ru,en}`) |
|
||||||
|
| `tooltip` | пусто | ML |
|
||||||
|
| `comment` | пусто | строка |
|
||||||
|
| `fillChecking` | `DontCheck` | `DontCheck` / `ShowError` / `ShowWarning` (то же, что флаг `req`) |
|
||||||
|
| `fullTextSearch` | `Use` | `Use` / `DontUse` |
|
||||||
|
| `fillFromFillingValue` | `false` | bool |
|
||||||
|
| `fillValue` | по типу (см. ниже) | значение заполнения |
|
||||||
|
| `createOnInput` | `Auto` | `Auto` / `Use` / `DontUse` |
|
||||||
|
| `quickChoice` | `Auto` | `Auto` / `Use` / `DontUse` |
|
||||||
|
| `choiceHistoryOnInput` | `Auto` | `Auto` / `DontUse` |
|
||||||
|
| `dataHistory` | `Use` | `Use` / `DontUse` |
|
||||||
|
| `use` | `ForItem` | `ForItem` / `ForFolder` / `ForFolderAndItem` (реквизит иерархического справочника) |
|
||||||
|
| `passwordMode` | `false` | bool |
|
||||||
|
| `multiLine` | `false` | bool (то же, что флаг `multiline`) |
|
||||||
|
| `extendedEdit` | `false` | bool (расширенное редактирование — многострочный ввод) |
|
||||||
|
| `mask` | пусто | строка маски ввода |
|
||||||
|
| `format` / `editFormat` | пусто | форматная строка 1С (ML) |
|
||||||
|
| `markNegatives` | `false` | bool (выделять отрицательные, для Number) |
|
||||||
|
| `minValue` / `maxValue` | не задано | граница диапазона (см. ниже) |
|
||||||
|
| `choiceParameterLinks` | пусто | связи параметров выбора (см. ниже) |
|
||||||
|
| `choiceParameters` | пусто | параметры выбора (см. ниже) |
|
||||||
|
| `choiceForm` | пусто | ссылка на форму выбора `Тип.Объект.Form.ИмяФормы` |
|
||||||
|
| `choiceFoldersAndItems` | `Items` | `Items` / `Folders` / `FoldersAndItems` (что выбирать в иерарх. справочнике) |
|
||||||
|
|
||||||
|
Индексирование задаётся флагом `index` / `indexAdditional` в shorthand, либо в объекте — как и в строковой форме,
|
||||||
|
через `"type": "… | index"`.
|
||||||
|
|
||||||
|
### `fillValue` — значение заполнения
|
||||||
|
|
||||||
|
Пустое значение по типу компилятор подставляет сам — ключ **не задают**:
|
||||||
|
|
||||||
|
| Тип реквизита | Пустое значение |
|
||||||
|
|---------------|-----------------|
|
||||||
|
| String | пустая строка |
|
||||||
|
| Number | `0` |
|
||||||
|
| Boolean, Date, ссылочный, составной | не задано (nil) |
|
||||||
|
|
||||||
|
Ключ `fillValue` задают для **конкретного** значения — интерпретируется по типу реквизита:
|
||||||
|
|
||||||
|
- **Boolean** — `true` / `false`.
|
||||||
|
- **Number** — число (`21`, `1.5`).
|
||||||
|
- **String** — строка.
|
||||||
|
- **Date** — ISO-строка `"2020-01-01T00:00:00"`.
|
||||||
|
- **Ссылочный** — путь: `"Catalog.Валюты.EmptyRef"` (пустая ссылка), `"Enum.Периодичность.EnumValue.Месяц"`
|
||||||
|
(значение перечисления), `"Catalog.СтраныМира.Россия"` (предопределённый элемент).
|
||||||
|
- **`null`** — явно «значение не задано» (nil), когда нужно перекрыть непустой дефолт типа.
|
||||||
|
- **`{ "emptyRef": true }`** — пустая ссылка для реквизита типа `DefinedType.X` (когда тип из пути не выводится).
|
||||||
|
|
||||||
|
> Пустая ссылка (`EmptyRef`) и `null` — разное: платформа хранит их отдельно.
|
||||||
|
|
||||||
|
### `minValue` / `maxValue` — границы диапазона
|
||||||
|
|
||||||
|
Число → числовая граница; строка → строковая (напр. год `"2000"`). Без ключа — граница не задана.
|
||||||
|
|
||||||
|
### `choiceParameterLinks` — связи параметров выбора
|
||||||
|
|
||||||
|
Связывают параметр выбора этого реквизита с другим реквизитом объекта. Массив строк или объектов:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"choiceParameterLinks": ["Отбор.Организация=Организация", "Отбор.Договор=Договор:DontChange"]
|
||||||
|
"choiceParameterLinks": [{ "name": "Отбор.Организация", "dataPath": "Организация", "valueChange": "Clear" }]
|
||||||
|
```
|
||||||
|
|
||||||
|
- `dataPath` — реквизит **того же объекта**: имя обычного реквизита (`"Организация"`) или стандартного
|
||||||
|
(`"Владелец"`, `"Ссылка"`).
|
||||||
|
- `valueChange` — `Clear` (по умолчанию) / `DontChange`.
|
||||||
|
|
||||||
|
### `choiceParameters` — параметры выбора
|
||||||
|
|
||||||
|
Фиксируют параметр выбора значением. Массив строк или объектов:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"choiceParameters": ["Отбор.ЭтоГруппа=false"]
|
||||||
|
"choiceParameters": [{ "name": "Отбор.Владелец", "value": "Catalog.Организации.EmptyRef" }]
|
||||||
|
```
|
||||||
|
|
||||||
|
- `value` — bool / число / строка / ссылочный путь (несёт тип) ИЛИ массив (список фиксированных значений).
|
||||||
|
- Для набора голых имён-значений добавьте `type` (тип поля-фильтра), чтобы они стали ссылками:
|
||||||
|
`{ "name": "Отбор.Тип", "type": "EnumRef.ТипыВЕТИС", "value": ["EmptyRef", "ТТН"] }`.
|
||||||
|
|
||||||
|
### Редкие ключи
|
||||||
|
|
||||||
|
`linkByType` — связь по типу (тип реквизита-Характеристики берётся из другого реквизита):
|
||||||
|
`{ "dataPath": "Свойство", "linkItem": 0 }` или строка-путь. Применяется для реквизитов-характеристик.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Табличная часть — объектная форма
|
||||||
|
|
||||||
|
Значение в `tabularSections` — массив колонок ЛИБО объект со свойствами самой ТЧ:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"tabularSections": {
|
||||||
|
"Товары": {
|
||||||
|
"synonym": { "ru": "Товары", "en": "Goods" },
|
||||||
|
"tooltip": "Строки заказа",
|
||||||
|
"fillChecking": "ShowError",
|
||||||
|
"attributes": ["Номенклатура: CatalogRef.Номенклатура", "Количество: Number(15,3)"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `synonym` | из имени | ML |
|
||||||
|
| `tooltip` | пусто | ML |
|
||||||
|
| `comment` | пусто | строка |
|
||||||
|
| `fillChecking` | `DontCheck` | `DontCheck` / `ShowError` / `ShowWarning` (обязательность заполнения ТЧ) |
|
||||||
|
| `use` | `ForItem` | `ForItem` / `ForFolder` / `ForFolderAndItem` (только Catalog / ChartOfCharacteristicTypes) |
|
||||||
|
| `attributes` | `[]` | колонки (shorthand или объектная форма реквизита) |
|
||||||
|
| `lineNumber` | — | кастомизация стандартного реквизита НомерСтроки (см. ниже) |
|
||||||
|
| `lineNumberLength` | по режиму совместимости | `5`…`9` — разрядность номера строки: `5` → до 99 999 строк, `9` → до 999 999 999. Требует формата 2.20 (платформа 8.3.27) |
|
||||||
|
|
||||||
|
### `lineNumber` — стандартный реквизит НомерСтроки
|
||||||
|
|
||||||
|
У каждой ТЧ есть стандартный реквизит НомерСтроки. По умолчанию все его свойства типовые. Ключ `lineNumber`
|
||||||
|
на объектной форме ТЧ их переопределяет:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"Строки": { "lineNumber": { "synonym": "Номер п/п", "fullTextSearch": "DontUse" }, "attributes": [...] }
|
||||||
|
```
|
||||||
|
|
||||||
|
Переопределяемые: `synonym`, `comment`, `fullTextSearch` (`Use`/`DontUse`), `tooltip`, `format`, `editFormat`,
|
||||||
|
`choiceHistoryOnInput` (`Auto`/`DontUse`).
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
# Блоки объекта
|
||||||
|
|
||||||
|
Кросс-типовые блоки уровня объекта (применимы к ссылочным типам — Catalog, Document, ChartOf*, ExchangePlan,
|
||||||
|
BusinessProcess, Task и др.).
|
||||||
|
|
||||||
|
## Представления
|
||||||
|
|
||||||
|
Тексты представления объекта в интерфейсе (ML — строка или `{ru,en}`, по умолчанию пусто):
|
||||||
|
|
||||||
|
| Ключ | Смысл |
|
||||||
|
|------|-------|
|
||||||
|
| `objectPresentation` | представление объекта |
|
||||||
|
| `extendedObjectPresentation` | расширенное представление объекта |
|
||||||
|
| `listPresentation` | представление списка |
|
||||||
|
| `extendedListPresentation` | расширенное представление списка |
|
||||||
|
| `explanation` | пояснение |
|
||||||
|
|
||||||
|
Набор доступных ключей зависит от типа (у списочных без формы объекта нет `objectPresentation` и т.п.).
|
||||||
|
|
||||||
|
```json
|
||||||
|
"listPresentation": "Организации", "objectPresentation": { "ru": "Организация", "en": "Company" }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Команды
|
||||||
|
|
||||||
|
Команды объекта. Ключ — имя команды, значение — объект свойств (map `имя → объект` или массив `[{name, …}]`).
|
||||||
|
Для каждой команды создаётся заготовка модуля с обработчиком `ОбработкаКоманды`.
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `synonym` | из имени | ML |
|
||||||
|
| `tooltip` | пусто | ML |
|
||||||
|
| `comment` | пусто | строка |
|
||||||
|
| `group` | **обязательно** | группа размещения (см. ниже) |
|
||||||
|
| `commandParameterType` | пусто | тип параметра (напр. `CatalogRef.Номенклатура`) — **только для групп формы** |
|
||||||
|
| `parameterUseMode` | `Single` | `Single` / `Multiple` |
|
||||||
|
| `modifiesData` | `false` | bool |
|
||||||
|
| `representation` | `Auto` | вид отображения |
|
||||||
|
| `picture` | пусто | ссылка на картинку (`StdPicture.Print`, `CommonPicture.Загрузка`) |
|
||||||
|
| `shortcut` | пусто | сочетание клавиш |
|
||||||
|
|
||||||
|
```json
|
||||||
|
"commands": {
|
||||||
|
"ПечатьЭтикеток": { "synonym": "Печать этикеток", "group": "FormCommandBarImportant",
|
||||||
|
"commandParameterType": "CatalogRef.Номенклатура", "picture": "StdPicture.Print" }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Группа (`group`) обязательна** — каждая команда размещается в группе командного интерфейса:
|
||||||
|
|
||||||
|
- **Командный интерфейс раздела** (панель навигации / панель действий; `commandParameterType` **недоступен**):
|
||||||
|
`NavigationPanelImportant` / `NavigationPanelOrdinary` / `NavigationPanelSeeAlso`,
|
||||||
|
`ActionsPanelCreate` / `ActionsPanelReports` / `ActionsPanelTools`.
|
||||||
|
- **Командный интерфейс формы** (`commandParameterType` допустим): `FormCommandBarImportant` /
|
||||||
|
`FormCommandBarCreateBasedOn`, `FormNavigationPanelImportant` / `FormNavigationPanelGoTo` / `FormNavigationPanelSeeAlso`.
|
||||||
|
- **Кастомная группа:** `CommandGroup.<Имя>` (параметр допустим).
|
||||||
|
|
||||||
|
Группа раздела вместе с `commandParameterType` → ошибка.
|
||||||
|
|
||||||
|
## `inputByString` / `dataLockFields` / `basedOn`
|
||||||
|
|
||||||
|
Списки полей/объектов уровня объекта. Поля — по имени реквизита объекта (обычного или стандартного).
|
||||||
|
|
||||||
|
- **`inputByString`** — поля быстрого ввода по строке. По умолчанию выводятся из Кода/Наименования — ключ не нужен;
|
||||||
|
задать при другом наборе/порядке, либо `[]` для отключения.
|
||||||
|
```json
|
||||||
|
"inputByString": ["Код", "Наименование", "Контрагент"]
|
||||||
|
```
|
||||||
|
- **`dataLockFields`** — поля управляемой блокировки данных (по умолчанию пусто).
|
||||||
|
```json
|
||||||
|
"dataLockFields": ["Организация", "Контрагент"]
|
||||||
|
```
|
||||||
|
- **`basedOn`** — «ввод на основании»: список ссылок на объекты метаданных (по умолчанию пусто).
|
||||||
|
```json
|
||||||
|
"basedOn": ["Catalog.Контрагенты", "Document.ЗаказПоставщику"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## `standardAttributes` — кастомизация стандартных реквизитов
|
||||||
|
|
||||||
|
Стандартные реквизиты объекта (Наименование, Код, Владелец, …) переопределяются блоком
|
||||||
|
`standardAttributes` — объект `{ ИмяРеквизита: { переопределения } }`. Имена — как в 1С: `Description`, `Code`,
|
||||||
|
`Owner`, `Parent`, `DeletionMark`, `Ref` и т.д. (для Document — `Date`, `Number`, `Posted`).
|
||||||
|
|
||||||
|
Переопределяемые поля — как у обычного реквизита (`synonym`, `tooltip`, `fillChecking`, `fillValue`,
|
||||||
|
`choiceParameters`, `comment`, `mask`, `choiceForm`; полный набор — `attributes.md`).
|
||||||
|
|
||||||
|
```json
|
||||||
|
"standardAttributes": {
|
||||||
|
"Description": { "synonym": "Наименование контрагента" },
|
||||||
|
"Code": { "fillChecking": "ShowError" }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## `characteristics` — «Дополнительные реквизиты и сведения»
|
||||||
|
|
||||||
|
Привязка плана видов характеристик. Массив; каждый элемент связывает **источник типов** (где определены
|
||||||
|
характеристики) и **источник значений** (где хранятся значения).
|
||||||
|
|
||||||
|
```json
|
||||||
|
"characteristics": [{
|
||||||
|
"types": { "from": "Catalog.НаборыДопРеквизитов.ДополнительныеРеквизиты",
|
||||||
|
"key": "Свойство", "filterField": "Ссылка", "filterValue": "Справочник_Организации" },
|
||||||
|
"values": { "from": "Catalog.Организации.TabularSection.ДополнительныеРеквизиты",
|
||||||
|
"object": "Ссылка", "type": "Свойство", "value": "Значение" }
|
||||||
|
}]
|
||||||
|
```
|
||||||
|
|
||||||
|
- `from` — таблица-источник; `key`/`filterField`/`object`/`type`/`value` — поля источника (по имени реквизита).
|
||||||
|
- `filterValue` — значение фильтра типов: имя предопределённого набора (строка) или путь к элементу.
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# Catalog (Справочник)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "Catalog", "name": "Организации", "descriptionLength": 100,
|
||||||
|
"attributes": ["ИНН: String(12)", "КПП: String(9)"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Свойства
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `comment` | пусто | строка |
|
||||||
|
| `hierarchical` | `false` | bool |
|
||||||
|
| `hierarchyType` | `HierarchyFoldersAndItems` | `HierarchyFoldersAndItems` / `HierarchyOfItems` |
|
||||||
|
| `limitLevelCount` | `false` | bool (ограничивать кол-во уровней) |
|
||||||
|
| `levelCount` | `2` | число уровней (при `limitLevelCount`) |
|
||||||
|
| `foldersOnTop` | `true` | bool (группы сверху) |
|
||||||
|
| `owners` | `[]` | массив владельцев: `["Catalog.Контрагенты"]` |
|
||||||
|
| `subordinationUse` | `ToItems` | `ToItems` / `ToFolders` / `ToFoldersAndItems` (кому подчинён) |
|
||||||
|
| `codeLength` | `9` | длина кода (0 — без кода) |
|
||||||
|
| `codeType` | `String` | `String` / `Number` |
|
||||||
|
| `codeAllowedLength` | `Variable` | `Variable` / `Fixed` |
|
||||||
|
| `codeSeries` | `WholeCatalog` | `WholeCatalog` / `WithinSubordination` / `WithinOwnerSubordination` |
|
||||||
|
| `autonumbering` | `true` | bool (автонумерация) |
|
||||||
|
| `checkUnique` | `false` | bool (контроль уникальности кода) |
|
||||||
|
| `descriptionLength` | `25` | длина наименования |
|
||||||
|
| `defaultPresentation` | `AsDescription` | `AsDescription` / `AsCode` |
|
||||||
|
| `quickChoice` | `true` | bool (быстрый выбор) |
|
||||||
|
| `choiceMode` | `BothWays` | `BothWays` / `QuickChoice` / `FromForm` |
|
||||||
|
| `editType` | `InDialog` | `InDialog` / `InList` / `BothWays` |
|
||||||
|
| `createOnInput` | `Use` | `Auto` / `Use` / `DontUse` |
|
||||||
|
| `choiceHistoryOnInput` | `Auto` | `Auto` / `DontUse` |
|
||||||
|
| `fullTextSearch` | `Use` | `Use` / `DontUse` |
|
||||||
|
| `fullTextSearchOnInputByString` | `DontUse` | `Use` / `DontUse` |
|
||||||
|
| `searchStringModeOnInputByString` | `Begin` | `Begin` / `AnyPart` |
|
||||||
|
| `predefinedDataUpdate` | `Auto` | `Auto` / `DontAutoUpdate` / `AutoUpdate` |
|
||||||
|
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
|
||||||
|
| `useStandardCommands` | `true` | bool |
|
||||||
|
| `includeHelpInContents` | `false` | bool |
|
||||||
|
| `attributes` | `[]` | реквизиты (shorthand / объектная форма) |
|
||||||
|
| `tabularSections` | `{}` | табличные части |
|
||||||
|
|
||||||
|
**Формы.** Ссылка на форму — `Тип.Объект.Form.ИмяФормы` (напр. `Catalog.Организации.Form.ФормаЭлемента`).
|
||||||
|
Слоты основных форм: `defaultObjectForm`, `defaultFolderForm`, `defaultListForm`, `defaultChoiceForm`,
|
||||||
|
`defaultFolderChoiceForm`; вспомогательных — те же имена с префиксом `auxiliary` (`auxiliaryObjectForm`, …).
|
||||||
|
|
||||||
|
## `predefined` — предопределённые элементы
|
||||||
|
|
||||||
|
Массив предопределённых элементов → `Ext/Predefined.xml`. Элемент — строка (плоский случай) или объект (иерархия).
|
||||||
|
|
||||||
|
**Строка:** `"(Код) Имя [Наименование]"` — `Имя` обязательно; `(Код)` и `[Наименование]` опциональны.
|
||||||
|
Без `[...]` наименование выводится из имени; `[]` — пустое; `[текст]` — заданное.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"predefined": [
|
||||||
|
"Основной",
|
||||||
|
"(1) ДокументОПриемке [Документ о приемке]",
|
||||||
|
{ "name": "Группа1", "isFolder": true, "description": "Прочие",
|
||||||
|
"childItems": ["Факс", "(7) Скайп"] }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Объект:** `name` (обязательно), `code`, `description` (наименование), `isFolder` (признак группы),
|
||||||
|
`childItems` (вложенные, рекурсивно). Тип кода — по свойству `codeType`.
|
||||||
|
|
||||||
|
## Дополнительно
|
||||||
|
|
||||||
|
- Свойства реквизитов и табличных частей — `attributes.md`.
|
||||||
|
- Представления (`objectPresentation`, `listPresentation`, …), команды объекта, характеристики
|
||||||
|
(«ДопРеквизиты и сведения»), кастомизация стандартных реквизитов, `inputByString` / `dataLockFields` /
|
||||||
|
`basedOn` — `blocks.md`.
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
# Планы: ChartOfCharacteristicTypes, ChartOfAccounts, ChartOfCalculationTypes
|
||||||
|
|
||||||
|
Все три — ссылочные типы (наследуют слой Catalog: коды, `standardAttributes`, `characteristics`, `inputByString`,
|
||||||
|
формы, представления — см. `catalog.md` / `attributes.md` / `blocks.md`) с предопределёнными элементами и своими
|
||||||
|
специальными свойствами.
|
||||||
|
|
||||||
|
## ChartOfCharacteristicTypes (План видов характеристик)
|
||||||
|
|
||||||
|
Хранит определения характеристик (видов). Иерархический (папки+элементы).
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `valueType` | любой примитив | тип значения характеристики (составной — строка `"A + B"` или массив `valueTypes`) |
|
||||||
|
| `characteristicExtValues` | пусто | ссылка на справочник доп. значений |
|
||||||
|
| `hierarchical` | `false` | bool |
|
||||||
|
| `foldersOnTop` | `true` | bool |
|
||||||
|
| `codeLength` | `9` | длина кода |
|
||||||
|
| `descriptionLength` | `100` | длина наименования |
|
||||||
|
| `checkUnique` | `true` | bool |
|
||||||
|
| `autonumbering` | `true` | bool |
|
||||||
|
| `codeSeries` | `WholeCharacteristicKind` | серия кодов |
|
||||||
|
| `createOnInput` | `DontUse` | `Auto` / `Use` / `DontUse` |
|
||||||
|
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
|
||||||
|
| `predefined` | `[]` | предопределённые виды (несут тип значения — см. ниже) |
|
||||||
|
| `attributes` / `tabularSections` | `[]` / `{}` | реквизиты / табличные части |
|
||||||
|
|
||||||
|
**Предопределённые виды** несут **тип значения на элемент** — короткой строкой после `:`
|
||||||
|
(`"(Код) Имя [Наименование]: Тип"`, составной через `+`) или объектной формой с ключом `type`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"predefined": [
|
||||||
|
"(000001) Цвет: CatalogRef.Цвета",
|
||||||
|
"(000002) Размер [Размер одежды]: String(50) + Number(3,0)",
|
||||||
|
{ "name": "Группа", "isFolder": true, "type": "" }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## ChartOfAccounts (План счетов)
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `extDimensionTypes` | пусто | ссылка на ПВХ видов субконто `ChartOfCharacteristicTypes.X` |
|
||||||
|
| `maxExtDimensionCount` | `0` (без ПВХ) / `3` (с ПВХ) | макс. число субконто |
|
||||||
|
| `codeMask` | пусто | маска кода счёта (напр. `"@@@.@@"`) |
|
||||||
|
| `codeLength` | `9` | длина кода |
|
||||||
|
| `descriptionLength` | `25` | длина наименования |
|
||||||
|
| `checkUnique` | `true` | bool |
|
||||||
|
| `codeSeries` | `WholeChartOfAccounts` | серия кодов |
|
||||||
|
| `defaultPresentation` | `AsCode` | `AsCode` / `AsDescription` |
|
||||||
|
| `autoOrderByCode` | `true` | bool |
|
||||||
|
| `orderLength` | `9` | длина строки упорядочивания |
|
||||||
|
| `createOnInput` | `DontUse` | `Auto` / `Use` / `DontUse` |
|
||||||
|
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
|
||||||
|
| `accountingFlags` | `[]` | признаки учёта (как реквизиты, тип по умолчанию Boolean; массив имён/реквизитов) |
|
||||||
|
| `extDimensionAccountingFlags` | `[]` | признаки учёта субконто (как реквизиты) |
|
||||||
|
| `predefined` | `[]` | предопределённые счета (см. ниже) |
|
||||||
|
|
||||||
|
**Предопределённый счёт** (объектная форма):
|
||||||
|
|
||||||
|
| Поле | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `name` | — | имя (обязательно) |
|
||||||
|
| `code` | пусто | код счёта |
|
||||||
|
| `description` | из имени | наименование |
|
||||||
|
| `accountType` | `ActivePassive` | `Active` / `Passive` / `ActivePassive` |
|
||||||
|
| `offBalance` | `false` | bool (забалансовый) |
|
||||||
|
| `order` | — | строка сортировки |
|
||||||
|
| `flags` | `[]` | включённые признаки учёта (только TRUE) |
|
||||||
|
| `subconto` | `[]` | виды субконто (см. ниже) |
|
||||||
|
| `childItems` | `[]` | подчинённые счета |
|
||||||
|
|
||||||
|
`subconto` — строка `"Вид | Признак1, Признак2"` (после `|` — включённые признаки учёта субконто; токен `Turnover` —
|
||||||
|
«только обороты») или объект `{ type, turnover, flags }`. `Вид` — имя предопределённого вида из ПВХ `extDimensionTypes`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"predefined": [
|
||||||
|
{ "name": "ОсновныеСредства", "code": "01", "accountType": "Active", "order": " 01",
|
||||||
|
"flags": ["Количественный"], "subconto": ["Номенклатура | Суммовой, Валютный"],
|
||||||
|
"childItems": [ { "name": "ОСВОрганизации", "code": "01.01", "accountType": "Active", "order": " 01.01" } ] }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## ChartOfCalculationTypes (План видов расчёта)
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `codeLength` | `5` | длина кода |
|
||||||
|
| `descriptionLength` | `100` | длина наименования |
|
||||||
|
| `codeAllowedLength` | `Variable` | `Variable` / `Fixed` |
|
||||||
|
| `dependenceOnCalculationTypes` | `DontUse` | `DontUse` / `OnPeriod` / `OnActionPeriod` |
|
||||||
|
| `baseCalculationTypes` | `[]` | базовые виды расчёта (список ссылок `ChartOfCalculationTypes.X`) |
|
||||||
|
| `actionPeriodUse` | `false` | bool (использовать период действия) |
|
||||||
|
| `createOnInput` | `DontUse` | `Auto` / `Use` / `DontUse` |
|
||||||
|
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
|
||||||
|
| `predefined` | `[]` | предопределённые виды расчёта (см. ниже) |
|
||||||
|
|
||||||
|
**Предопределённый вид расчёта** — плоский: строка `"(Код) Имя [Наименование]"` или объект
|
||||||
|
`{ name, code, description, actionPeriodIsBase }` (`actionPeriodIsBase` — bool, по умолчанию `false`).
|
||||||
|
|
||||||
|
```json
|
||||||
|
"predefined": [ "(00001) Оклад [Оклад по дням]", { "name": "Премия", "code": "00002", "actionPeriodIsBase": true } ]
|
||||||
|
```
|
||||||
|
|
||||||
|
> **ChartOfAccounts** ссылается на ПВХ через `extDimensionTypes`. Регистр бухгалтерии/расчёта требует
|
||||||
|
> соответствующий план (см. `registers.md`).
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# CommonModule, ScheduledJob, EventSubscription (объекты, привязанные к коду)
|
||||||
|
|
||||||
|
## CommonModule (Общий модуль)
|
||||||
|
|
||||||
|
Флаги контекста выполнения (все bool, по умолчанию `false`). Создаёт пустой `Ext/Module.bsl`.
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `context` | — | шорткат флагов (см. ниже) |
|
||||||
|
| `global` | `false` | bool |
|
||||||
|
| `server` | `false` | bool |
|
||||||
|
| `serverCall` | `false` | bool (вызов сервера) |
|
||||||
|
| `clientManagedApplication` | `false` | bool (клиент управляемого приложения) |
|
||||||
|
| `clientOrdinaryApplication` | `false` | bool (клиент обычного приложения) |
|
||||||
|
| `externalConnection` | `false` | bool |
|
||||||
|
| `privileged` | `false` | bool |
|
||||||
|
| `returnValuesReuse` | `DontUse` | `DontUse` / `DuringRequest` / `DuringSession` |
|
||||||
|
|
||||||
|
Шорткат `context`: `"server"` → Server+ServerCall; `"client"` → ClientManagedApplication;
|
||||||
|
`"serverClient"` → Server+ClientManagedApplication.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "CommonModule", "name": "ОбменДаннымиСервер", "context": "server", "returnValuesReuse": "DuringRequest" }
|
||||||
|
```
|
||||||
|
|
||||||
|
## ScheduledJob (Регламентное задание)
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `methodName` | пусто | метод-обработчик `"МодульСервер.Процедура"` (дополняется до `CommonModule.…`) |
|
||||||
|
| `description` | пусто | наименование задания |
|
||||||
|
| `key` | пусто | ключ |
|
||||||
|
| `use` | `false` | bool (использование) |
|
||||||
|
| `predefined` | `false` | bool (предопределённое) |
|
||||||
|
| `restartCountOnFailure` | `3` | число повторов при сбое |
|
||||||
|
| `restartIntervalOnFailure` | `10` | интервал повтора, сек |
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "ScheduledJob", "name": "ОбменДанными", "methodName": "ОбменДаннымиСервер.Выполнить", "use": true }
|
||||||
|
```
|
||||||
|
|
||||||
|
## EventSubscription (Подписка на событие)
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `source` | `[]` | объекты-источники: `["CatalogObject.Контрагенты", "DocumentObject.Реализация"]` |
|
||||||
|
| `event` | `BeforeWrite` | `BeforeWrite` / `OnWrite` / `BeforeDelete` / `OnReadAtServer` / `FillCheckProcessing` … |
|
||||||
|
| `handler` | пусто | метод-обработчик `"МодульСервер.Процедура"` (дополняется до `CommonModule.…`) |
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "EventSubscription", "name": "ПередЗаписьюКонтрагента",
|
||||||
|
"source": ["CatalogObject.Контрагенты"], "event": "BeforeWrite",
|
||||||
|
"handler": "ОбщегоНазначенияСервер.ПередЗаписьюКонтрагента" }
|
||||||
|
```
|
||||||
|
|
||||||
|
> Процедура-обработчик (`methodName` / `handler`) должна существовать в указанном общем модуле (экспортная).
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# Document, DocumentJournal, Sequence, DocumentNumerator
|
||||||
|
|
||||||
|
## Document (Документ)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "Document", "name": "ПриходнаяНакладная",
|
||||||
|
"registerRecords": ["AccumulationRegister.ОстаткиТоваров"],
|
||||||
|
"attributes": ["Организация: CatalogRef.Организации"],
|
||||||
|
"tabularSections": { "Товары": ["Номенклатура: CatalogRef.Номенклатура", "Количество: Number(15,3)"] } }
|
||||||
|
```
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `comment` | пусто | строка |
|
||||||
|
| `numerator` | пусто | ссылка на нумератор `DocumentNumerator.X` |
|
||||||
|
| `numberType` | `String` | `String` / `Number` |
|
||||||
|
| `numberLength` | `11` | длина номера |
|
||||||
|
| `numberAllowedLength` | `Variable` | `Variable` / `Fixed` |
|
||||||
|
| `numberPeriodicity` | `Year` | `Nonperiodical` / `Day` / `Month` / `Quarter` / `Year` |
|
||||||
|
| `checkUnique` | `true` | bool |
|
||||||
|
| `autonumbering` | `true` | bool |
|
||||||
|
| `posting` | `Allow` | `Allow` / `Deny` (проведение) |
|
||||||
|
| `realTimePosting` | `Deny` | `Allow` / `Deny` (оперативное проведение) |
|
||||||
|
| `registerRecordsDeletion` | `AutoDelete` | `AutoDelete` / `AutoDeleteOnUnpost` / `AutoDeleteOff` |
|
||||||
|
| `registerRecordsWritingOnPost` | `WriteSelected` | `WriteModified` / `WriteSelected` / `WriteAll` |
|
||||||
|
| `sequenceFilling` | `AutoFill` | заполнение последовательностей |
|
||||||
|
| `postInPrivilegedMode` | `true` | bool |
|
||||||
|
| `unpostInPrivilegedMode` | `true` | bool |
|
||||||
|
| `createOnInput` | `Use` | `Auto` / `Use` / `DontUse` |
|
||||||
|
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
|
||||||
|
| `fullTextSearch` | `Use` | `Use` / `DontUse` |
|
||||||
|
| `dataHistory` | `DontUse` | `Use` / `DontUse` |
|
||||||
|
| `registerRecords` | `[]` | движения: список ссылок `["AccumulationRegister.ОстаткиТоваров", "InformationRegister.Цены"]` |
|
||||||
|
| `useStandardCommands` | `true` | bool |
|
||||||
|
| `includeHelpInContents` | `false` | bool |
|
||||||
|
| `attributes` / `tabularSections` | `[]` / `{}` | реквизиты / табличные части |
|
||||||
|
|
||||||
|
Формы: `defaultObjectForm`, `defaultListForm`, `defaultChoiceForm`, `auxiliary*` (см. `catalog.md`).
|
||||||
|
Реквизиты и ТЧ — `attributes.md`. Представления, команды, характеристики, `basedOn`, `standardAttributes`,
|
||||||
|
`inputByString`, `dataLockFields` — `blocks.md`.
|
||||||
|
|
||||||
|
## DocumentJournal (Журнал документов)
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `registeredDocuments` | `[]` | документы журнала: `["Document.Встреча", "Document.Звонок"]` |
|
||||||
|
| `columns` | `[]` | графы журнала (см. ниже) |
|
||||||
|
|
||||||
|
Графа — строка `"Имя"` или объект `{ name, synonym, indexing, references }`, где `indexing` — `Index`/`DontIndex`,
|
||||||
|
`references` — пути к реквизитам документов, отображаемым в графе.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "DocumentJournal", "name": "Взаимодействия",
|
||||||
|
"registeredDocuments": ["Document.Встреча", "Document.Звонок"],
|
||||||
|
"columns": [{ "name": "Организация", "indexing": "Index",
|
||||||
|
"references": ["Document.Встреча.Attribute.Организация"] }] }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sequence (Последовательность документов)
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `moveBoundaryOnPosting` | `DontMove` | сдвиг границы при проведении |
|
||||||
|
| `documents` | `[]` | документы последовательности (список ссылок) |
|
||||||
|
| `registerRecords` | `[]` | движения (список ссылок) |
|
||||||
|
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
|
||||||
|
| `dimensions` | `[]` | измерения `{name, type, documentMap[], registerRecordsMap[]}` |
|
||||||
|
|
||||||
|
`documentMap` / `registerRecordsMap` — пути к реквизитам документов / движениям, соответствующим измерению.
|
||||||
|
|
||||||
|
## DocumentNumerator (Нумератор документов)
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `numberType` | `String` | `String` / `Number` |
|
||||||
|
| `numberLength` | `11` | длина номера |
|
||||||
|
| `numberAllowedLength` | `Variable` | `Variable` / `Fixed` |
|
||||||
|
| `numberPeriodicity` | `Year` | `Nonperiodical` / `Day` / … / `Year` |
|
||||||
|
| `checkUnique` | `true` | bool |
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# ExchangePlan (План обмена)
|
||||||
|
|
||||||
|
Близок к справочнику (без иерархии/владельцев), плюс состав объектов обмена. Наследует слой Catalog:
|
||||||
|
`codeLength`, `codeAllowedLength`, `descriptionLength`, `defaultPresentation`, `editType`, `quickChoice`,
|
||||||
|
`choiceMode`, формы, `standardAttributes`, `characteristics`, `inputByString`, `basedOn`, представления —
|
||||||
|
см. `catalog.md` / `attributes.md` / `blocks.md`.
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `comment` | пусто | строка |
|
||||||
|
| `distributedInfoBase` | `false` | bool (распределённая ИБ — РИБ) |
|
||||||
|
| `includeConfigurationExtensions` | `false` | bool (включать расширения конфигурации) |
|
||||||
|
| `descriptionLength` | `150` | длина наименования |
|
||||||
|
| `createOnInput` | `DontUse` | `Auto` / `Use` / `DontUse` |
|
||||||
|
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
|
||||||
|
| `dataHistory` | `DontUse` | `Use` / `DontUse` |
|
||||||
|
| `useStandardCommands` | `true` | bool |
|
||||||
|
| `content` | `[]` | состав обмена (см. ниже) |
|
||||||
|
| `attributes` / `tabularSections` | `[]` / `{}` | реквизиты / табличные части |
|
||||||
|
|
||||||
|
## `content` — состав плана обмена
|
||||||
|
|
||||||
|
Список объектов-участников обмена; у каждого — признак авторегистрации изменений (по умолчанию выключена).
|
||||||
|
Элемент — ссылка на объект метаданных (строка) или объект с признаком:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"content": [
|
||||||
|
"Catalog.Организации", // авторегистрация выключена
|
||||||
|
"InformationRegister.Курсы: autoRecord", // авторегистрация включена (токен)
|
||||||
|
{ "metadata": "Document.РеализацияТоваров", "autoRecord": true }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
- Строка `"Тип.Имя"` — авторегистрация выключена; суффикс `: autoRecord` — включена.
|
||||||
|
- Объект: `metadata` (ссылка), `autoRecord` (bool или `Allow`/`Deny`).
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "ExchangePlan", "name": "ОбменССайтом", "distributedInfoBase": false,
|
||||||
|
"content": ["Catalog.Номенклатура: autoRecord", "Catalog.Контрагенты: autoRecord"],
|
||||||
|
"attributes": ["АдресСервера: String(200)"] }
|
||||||
|
```
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# Прочие типы
|
||||||
|
|
||||||
|
Редкие/служебные объекты. Каждый — минимальный набор свойств.
|
||||||
|
|
||||||
|
## FunctionalOption (Функциональная опция)
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `location` | пусто | где хранится значение: `Constant.X` / `InformationRegister.X.Resource.Y` / `<Тип>.X.Attribute.Y` |
|
||||||
|
| `content` | `[]` | реквизиты/измерения/ресурсы, зависящие от опции (полные пути к объектам) |
|
||||||
|
| `privilegedGetMode` | `true` | bool |
|
||||||
|
| `comment` | пусто | строка |
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "FunctionalOption", "name": "ВестиУчетПоСкладам", "location": "Constant.ВестиУчетПоСкладам",
|
||||||
|
"content": ["Document.РеализацияТоваров.TabularSection.Товары.Attribute.Склад"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
## FilterCriterion (Критерий отбора)
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `valueType` | — | тип значения отбора (составной через `+`) |
|
||||||
|
| `content` | `[]` | реквизиты, по которым идёт отбор (пути к объектам) |
|
||||||
|
| `useStandardCommands` | `true` | bool |
|
||||||
|
| `defaultForm` / `auxiliaryForm` | пусто | формы |
|
||||||
|
| `comment` | пусто | строка |
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "FilterCriterion", "name": "ДокументыПоКонтрагенту", "valueType": "CatalogRef.Контрагенты",
|
||||||
|
"content": ["Document.Реализация.Attribute.Контрагент"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
## SettingsStorage (Хранилище настроек)
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `defaultSaveForm` / `defaultLoadForm` | пусто | формы сохранения / загрузки |
|
||||||
|
| `auxiliarySaveForm` / `auxiliaryLoadForm` | пусто | вспомогательные формы |
|
||||||
|
| `comment` | пусто | строка |
|
||||||
|
|
||||||
|
## CommonForm (Общая форма)
|
||||||
|
|
||||||
|
Создаёт метаданные + заготовку формы. Содержимое формы наполняется `/form-compile` или `/form-edit`.
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `formType` | `Managed` | тип формы |
|
||||||
|
| `usePurposes` | `[PlatformApplication, MobilePlatformApplication]` | назначение (массив) |
|
||||||
|
| `useStandardCommands` | `false` | bool |
|
||||||
|
| `includeHelpInContents` | `false` | bool |
|
||||||
|
| `comment` | пусто | строка |
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "CommonForm", "name": "НастройкиОбмена", "usePurposes": ["PlatformApplication"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
## CommonPicture / CommonTemplate (Общие картинки и макеты)
|
||||||
|
|
||||||
|
Только метаданные + регистрация; содержимое (`Ext/Picture*`, `Ext/Template.*`) импортируется отдельно
|
||||||
|
(для табличного макета — `/mxl-compile`).
|
||||||
|
|
||||||
|
- **CommonPicture** — `availabilityForChoice` / `availabilityForAppearance` (bool, по умолчанию `false`).
|
||||||
|
- **CommonTemplate** — `templateType` (`SpreadsheetDocument` по умолчанию / `TextDocument` / `HTMLDocument` /
|
||||||
|
`BinaryData` / `AddIn` / `DataCompositionSchema` / `DataCompositionAppearanceTemplate` / `GraphicalSchema`).
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "CommonTemplate", "name": "ПечатьЗаказа", "templateType": "SpreadsheetDocument" }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Служебные типы
|
||||||
|
|
||||||
|
- **SessionParameter** (параметр сеанса) — `valueType` (тип значения, составной через `+`).
|
||||||
|
- **FunctionalOptionsParameter** (параметр функциональной опции) — `use` (массив измерений/реквизитов).
|
||||||
|
- **WSReference** (WS-ссылка) — `locationURL` (URL WSDL).
|
||||||
|
- **CommandGroup** (группа команд) — `category` (по умолч. `NavigationPanel`) — где размещается группа:
|
||||||
|
`NavigationPanel` / `ActionsPanel` (командный интерфейс раздела) или `FormCommandBar` / `FormNavigationPanel`
|
||||||
|
(командный интерфейс формы); `representation` (`Auto`), `tooltip` (ML), `picture`. Команды объекта ссылаются на
|
||||||
|
группу через `group: "CommandGroup.<Имя>"` (см. `blocks.md`).
|
||||||
|
- **CommonCommand** (общая команда) — `group`, `representation`, `tooltip`, `picture`, `shortcut`,
|
||||||
|
`commandParameterType`, `parameterUseMode` (`Single`/`Multiple`), `modifiesData`, `includeHelpInContents`.
|
||||||
|
Создаёт `Ext/CommandModule.bsl`.
|
||||||
|
- **CommonAttribute** (общий реквизит) — `valueType` (по умолчанию `String(0)`) + свойства реквизита
|
||||||
|
(`attributes.md`) + `content` (объекты, куда входит реквизит) + свойства разделения данных
|
||||||
|
(`dataSeparation`, `separatedDataUse`, `usersSeparation`, … — по умолчанию `DontUse`/`Independently`).
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "CommonAttribute", "name": "Организация", "valueType": "CatalogRef.Организации",
|
||||||
|
"autoUse": "Use", "content": ["Document.РеализацияТоваров", "Document.ПоступлениеТоваров"] }
|
||||||
|
```
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# BusinessProcess, Task (Бизнес-процессы и Задачи)
|
||||||
|
|
||||||
|
Ссылочные типы. Наследуют слой Catalog (нумерация, формы, `standardAttributes`, `characteristics`, `basedOn`,
|
||||||
|
представления — см. `catalog.md` / `attributes.md` / `blocks.md`). Бизнес-процесс всегда связан с задачей.
|
||||||
|
|
||||||
|
## BusinessProcess (Бизнес-процесс)
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `task` | пусто | ссылка на задачу `Task.X` (обязательна для рабочего БП) |
|
||||||
|
| `numberType` | `String` | `String` / `Number` |
|
||||||
|
| `numberLength` | `11` | длина номера |
|
||||||
|
| `checkUnique` | `true` | bool |
|
||||||
|
| `autonumbering` | `true` | bool |
|
||||||
|
| `createOnInput` | `DontUse` | `Auto` / `Use` / `DontUse` |
|
||||||
|
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
|
||||||
|
| `attributes` / `tabularSections` | `[]` / `{}` | реквизиты / табличные части |
|
||||||
|
|
||||||
|
Создаётся с картой маршрута (`Ext/Flowchart.xml`) и модулем объекта.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "BusinessProcess", "name": "Согласование", "task": "Task.ЗадачаИсполнителя",
|
||||||
|
"attributes": ["Документ: DocumentRef.ЗаявкаНаРасход"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Task (Задача)
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `numberType` | `String` | `String` / `Number` |
|
||||||
|
| `numberLength` | `14` | длина номера |
|
||||||
|
| `checkUnique` | `true` | bool |
|
||||||
|
| `autonumbering` | `true` | bool |
|
||||||
|
| `descriptionLength` | `150` | длина наименования |
|
||||||
|
| `addressing` | пусто | ссылка на регистр сведений адресации `InformationRegister.X` |
|
||||||
|
| `mainAddressingAttribute` | пусто | основной реквизит адресации (имя реквизита адресации) |
|
||||||
|
| `currentPerformer` | пусто | реквизит текущего исполнителя |
|
||||||
|
| `createOnInput` | `DontUse` | `Auto` / `Use` / `DontUse` |
|
||||||
|
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
|
||||||
|
| `addressingAttributes` | `[]` | реквизиты адресации (см. ниже) |
|
||||||
|
| `attributes` / `tabularSections` | `[]` / `{}` | реквизиты / табличные части |
|
||||||
|
|
||||||
|
**Реквизит адресации** — shorthand `"Имя: Тип"` или объект `{ name, type, addressingDimension }`
|
||||||
|
(`addressingDimension` — измерение регистра адресации).
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "Task", "name": "ЗадачаИсполнителя",
|
||||||
|
"addressingAttributes": ["Исполнитель: CatalogRef.Пользователи", "Роль: CatalogRef.Роли"] }
|
||||||
|
```
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# Регистры: Information, Accumulation, Accounting, Calculation
|
||||||
|
|
||||||
|
**Измерения и ресурсы** задаются как реквизиты (shorthand `"Имя: Тип | флаги"` или объектная форма, см.
|
||||||
|
`attributes.md`). Флаги измерений: `master`, `mainFilter`, `denyIncomplete`, `useInTotals` (регистр накопления).
|
||||||
|
|
||||||
|
```json
|
||||||
|
"dimensions": ["Организация: CatalogRef.Организации | master, mainFilter"],
|
||||||
|
"resources": ["Сумма: Number(15,2)"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## InformationRegister (Регистр сведений)
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `writeMode` | `Independent` | `Independent` / `RecorderSubordinate` |
|
||||||
|
| `periodicity` | `Nonperiodical` | `Nonperiodical` / `Second` / `Day` / `Month` / `Quarter` / `Year` / `RecorderPosition` |
|
||||||
|
| `mainFilterOnPeriod` | `false` | bool (основной отбор по периоду) |
|
||||||
|
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
|
||||||
|
| `dimensions` / `resources` / `attributes` | `[]` | измерения / ресурсы / реквизиты |
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "InformationRegister", "name": "КурсыВалют", "periodicity": "Day",
|
||||||
|
"dimensions": ["Валюта: CatalogRef.Валюты | master, mainFilter, denyIncomplete"],
|
||||||
|
"resources": ["Курс: Number(15,4)", "Кратность: Number(10,0)"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
## AccumulationRegister (Регистр накопления)
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `registerType` | `Balance` | `Balance` (остатки) / `Turnovers` (обороты) |
|
||||||
|
| `enableTotalsSplitting` | `true` | bool (разделение итогов) |
|
||||||
|
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
|
||||||
|
| `dimensions` / `resources` / `attributes` | `[]` | измерения / ресурсы / реквизиты |
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "AccumulationRegister", "name": "ОстаткиТоваров", "registerType": "Balance",
|
||||||
|
"dimensions": ["Номенклатура: CatalogRef.Номенклатура", "Склад: CatalogRef.Склады"],
|
||||||
|
"resources": ["Количество: Number(15,3)"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
## AccountingRegister (Регистр бухгалтерии)
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `chartOfAccounts` | — | **обязательно**: ссылка на план счетов `ChartOfAccounts.X` |
|
||||||
|
| `correspondence` | `false` | bool (корреспонденция) |
|
||||||
|
| `periodAdjustmentLength` | `0` | длина периода корректировки |
|
||||||
|
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
|
||||||
|
| `dimensions` / `resources` / `attributes` | `[]` | измерения / ресурсы / реквизиты |
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "AccountingRegister", "name": "Хозрасчетный",
|
||||||
|
"chartOfAccounts": "ChartOfAccounts.Хозрасчетный",
|
||||||
|
"dimensions": ["Организация: CatalogRef.Организации"], "resources": ["Сумма: Number(15,2)"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
## CalculationRegister (Регистр расчёта)
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `chartOfCalculationTypes` | — | **обязательно**: ссылка на ПВР `ChartOfCalculationTypes.X` |
|
||||||
|
| `periodicity` | `Month` | периодичность |
|
||||||
|
| `actionPeriod` | `false` | bool (период действия) |
|
||||||
|
| `basePeriod` | `false` | bool (базовый период) |
|
||||||
|
| `schedule` | пусто | ссылка на регистр сведений графиков |
|
||||||
|
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
|
||||||
|
| `dimensions` / `resources` / `attributes` | `[]` | измерения / ресурсы / реквизиты |
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "CalculationRegister", "name": "Начисления",
|
||||||
|
"chartOfCalculationTypes": "ChartOfCalculationTypes.Начисления", "periodicity": "Month",
|
||||||
|
"dimensions": ["Сотрудник: CatalogRef.Сотрудники"], "resources": ["Сумма: Number(15,2)"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
> **AccountingRegister** требует план счетов, **CalculationRegister** — план видов расчёта (и оба — документ-регистратор).
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Report, DataProcessor (Отчёты и Обработки)
|
||||||
|
|
||||||
|
Почти идентичны по составу: реквизиты, табличные части, формы, макеты, команды. Модуль объекта — `Ext/ObjectModule.bsl`.
|
||||||
|
Реквизиты и ТЧ — `attributes.md`; команды — `blocks.md`.
|
||||||
|
|
||||||
|
Ссылки на формы/схемы/хранилища пишутся **как есть** (имя формы может быть буквально «Форма»).
|
||||||
|
|
||||||
|
## Report (Отчёт)
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `comment` | пусто | строка |
|
||||||
|
| `useStandardCommands` | `true` | bool (доступность через стандартный командный интерфейс) |
|
||||||
|
| `mainDataCompositionSchema` | пусто | основной макет СКД (`Report.X.Template.ОсновнаяСхемаКомпоновкиДанных`) |
|
||||||
|
| `defaultForm` / `auxiliaryForm` | пусто | основная / вспомогательная форма |
|
||||||
|
| `defaultSettingsForm` / `auxiliarySettingsForm` / `defaultVariantForm` | пусто | формы настроек / вариантов |
|
||||||
|
| `variantsStorage` / `settingsStorage` | пусто | хранилища вариантов / настроек (`SettingsStorage.X`) |
|
||||||
|
| `extendedPresentation` / `explanation` | пусто | представление / пояснение (ML) |
|
||||||
|
| `includeHelpInContents` | `false` | bool |
|
||||||
|
| `attributes` / `tabularSections` | `[]` / `{}` | реквизиты / табличные части |
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "Report", "name": "АнализПродаж", "useStandardCommands": false,
|
||||||
|
"mainDataCompositionSchema": "Report.АнализПродаж.Template.ОсновнаяСхемаКомпоновкиДанных",
|
||||||
|
"attributes": ["Период: StandardPeriod"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
## DataProcessor (Обработка)
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `comment` | пусто | строка |
|
||||||
|
| `useStandardCommands` | `true` | bool |
|
||||||
|
| `defaultForm` / `auxiliaryForm` | пусто | основная / вспомогательная форма |
|
||||||
|
| `extendedPresentation` / `explanation` | пусто | представление / пояснение (ML) |
|
||||||
|
| `includeHelpInContents` | `false` | bool |
|
||||||
|
| `attributes` / `tabularSections` | `[]` / `{}` | реквизиты / табличные части |
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "DataProcessor", "name": "ЗагрузкаТаблиц", "useStandardCommands": false,
|
||||||
|
"attributes": [{ "name": "Таблица", "type": "ValueTree" }, { "name": "Произвольные", "type": "" }] }
|
||||||
|
```
|
||||||
|
|
||||||
|
> Реквизиты отчётов/обработок допускают платформенные типы-коллекции: `ValueTable`, `ValueTree`, `ValueList`,
|
||||||
|
> `StandardPeriod`, `SpreadsheetDocument` и др., а также `"type": ""` — реквизит без типа.
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# Enum, Constant, DefinedType
|
||||||
|
|
||||||
|
## Enum (Перечисление)
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `values` | `[]` | значения перечисления (массив имён или объектов) |
|
||||||
|
|
||||||
|
Значение — строка `"ИмяЗначения"` или объект `{ name, synonym }`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "Enum", "name": "Статусы", "values": ["Новый", "ВРаботе", "Закрыт"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Constant (Константа)
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `valueType` | `String` | тип значения (shorthand типа) |
|
||||||
|
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
|
||||||
|
|
||||||
|
`valueType` принимает shorthand: `"String(100)"`, `"Number(15,2)"`, `"Boolean"`, `"CatalogRef.Валюты"`,
|
||||||
|
составной через `+`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "Constant", "name": "ОсновнаяВалюта", "valueType": "CatalogRef.Валюты" }
|
||||||
|
```
|
||||||
|
|
||||||
|
## DefinedType (Определяемый тип)
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `valueTypes` | `[]` | состав типа (массив shorthand-типов) |
|
||||||
|
| `valueType` | — | то же одной строкой (`"A + B"`) или строкой одного типа |
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "DefinedType", "name": "ДенежныеСредства",
|
||||||
|
"valueTypes": ["CatalogRef.БанковскиеСчета", "CatalogRef.Кассы"] }
|
||||||
|
{ "type": "DefinedType", "name": "ФлагАктивности", "valueType": "Boolean" }
|
||||||
|
```
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
# Базовые типы: Catalog, Document, Enum, Constant, DefinedType, Report, DataProcessor
|
|
||||||
|
|
||||||
## Catalog
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `hierarchical` | `false` | Hierarchical |
|
|
||||||
| `hierarchyType` | `HierarchyFoldersAndItems` | HierarchyType |
|
|
||||||
| `limitLevelCount` | `false` | LimitLevelCount |
|
|
||||||
| `levelCount` | `2` | LevelCount |
|
|
||||||
| `foldersOnTop` | `true` | FoldersOnTop |
|
|
||||||
| `codeLength` | `9` | CodeLength |
|
|
||||||
| `codeType` | `String` | CodeType |
|
|
||||||
| `codeAllowedLength` | `Variable` | CodeAllowedLength |
|
|
||||||
| `codeSeries` | `WholeCatalog` | CodeSeries |
|
|
||||||
| `descriptionLength` | `25` | DescriptionLength |
|
|
||||||
| `autonumbering` | `true` | Autonumbering |
|
|
||||||
| `checkUnique` | `false` | CheckUnique |
|
|
||||||
| `defaultPresentation` | `AsDescription` | DefaultPresentation |
|
|
||||||
| `subordinationUse` | `ToItems` | SubordinationUse |
|
|
||||||
| `quickChoice` | `false` | QuickChoice |
|
|
||||||
| `choiceMode` | `BothWays` | ChoiceMode |
|
|
||||||
| `owners` | `[]` | Owners |
|
|
||||||
| `attributes` | `[]` | → Attribute в ChildObjects |
|
|
||||||
| `tabularSections` | `{}` | → TabularSection в ChildObjects |
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "type": "Catalog", "name": "Организации", "attributes": ["ИНН: String(12)", "КПП: String(9)"] }
|
|
||||||
```
|
|
||||||
|
|
||||||
## Document
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `numberType` | `String` | NumberType |
|
|
||||||
| `numberLength` | `11` | NumberLength |
|
|
||||||
| `numberAllowedLength` | `Variable` | NumberAllowedLength |
|
|
||||||
| `numberPeriodicity` | `Year` | NumberPeriodicity |
|
|
||||||
| `checkUnique` | `true` | CheckUnique |
|
|
||||||
| `autonumbering` | `true` | Autonumbering |
|
|
||||||
| `posting` | `Allow` | Posting |
|
|
||||||
| `realTimePosting` | `Deny` | RealTimePosting |
|
|
||||||
| `registerRecordsDeletion` | `AutoDelete` | RegisterRecordsDeletion |
|
|
||||||
| `registerRecordsWritingOnPost` | `WriteModified` | RegisterRecordsWritingOnPost |
|
|
||||||
| `postInPrivilegedMode` | `true` | PostInPrivilegedMode |
|
|
||||||
| `unpostInPrivilegedMode` | `true` | UnpostInPrivilegedMode |
|
|
||||||
| `registerRecords` | `[]` | RegisterRecords |
|
|
||||||
| `attributes` | `[]` | → Attribute в ChildObjects |
|
|
||||||
| `tabularSections` | `{}` | → TabularSection в ChildObjects |
|
|
||||||
|
|
||||||
RegisterRecords — массив строк: `"AccumulationRegister.Продажи"`, `"InformationRegister.Цены"`.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "Document", "name": "ПриходнаяНакладная",
|
|
||||||
"registerRecords": ["AccumulationRegister.ОстаткиТоваров"],
|
|
||||||
"attributes": ["Организация: CatalogRef.Организации", "Контрагент: CatalogRef.Контрагенты"],
|
|
||||||
"tabularSections": { "Товары": ["Номенклатура: CatalogRef.Номенклатура", "Количество: Number(15,3)"] }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Enum
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `values` | `[]` | → EnumValue в ChildObjects |
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "type": "Enum", "name": "Статусы", "values": ["Новый", "ВРаботе", "Закрыт"] }
|
|
||||||
```
|
|
||||||
|
|
||||||
## Constant
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `valueType` | `String` | Type |
|
|
||||||
|
|
||||||
`valueType` принимает shorthand типа: `"String(100)"`, `"Number(15,2)"`, `"Boolean"`, `"CatalogRef.Валюты"`.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "type": "Constant", "name": "ОсновнаяВалюта", "valueType": "CatalogRef.Валюты" }
|
|
||||||
```
|
|
||||||
|
|
||||||
## DefinedType
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `valueTypes` | `[]` | Type (составной тип) |
|
|
||||||
| `valueType` | — | Алиас для `valueTypes` (строка или массив) |
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "type": "DefinedType", "name": "ДенежныеСредства", "valueTypes": ["CatalogRef.БанковскиеСчета", "CatalogRef.Кассы"] }
|
|
||||||
{ "type": "DefinedType", "name": "ФлагАктивности", "valueType": "Boolean" }
|
|
||||||
```
|
|
||||||
|
|
||||||
## Report
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `attributes` | `[]` | → Attribute в ChildObjects |
|
|
||||||
| `tabularSections` | `{}` | → TabularSection в ChildObjects |
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "type": "Report", "name": "ОстаткиТоваров" }
|
|
||||||
```
|
|
||||||
|
|
||||||
## DataProcessor
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `attributes` | `[]` | → Attribute в ChildObjects |
|
|
||||||
| `tabularSections` | `{}` | → TabularSection в ChildObjects |
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "type": "DataProcessor", "name": "ЗагрузкаДанных", "attributes": ["ПутьКФайлу: String(500)"] }
|
|
||||||
```
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
# Процессы и сервисные: BusinessProcess, Task, ExchangePlan, CommonModule, ScheduledJob, EventSubscription, DocumentJournal
|
|
||||||
|
|
||||||
## BusinessProcess
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `task` | `""` | Task (ссылка `Task.XXX`) |
|
|
||||||
| `numberType` | `String` | NumberType |
|
|
||||||
| `numberLength` | `11` | NumberLength |
|
|
||||||
| `checkUnique` | `true` | CheckUnique |
|
|
||||||
| `autonumbering` | `true` | Autonumbering |
|
|
||||||
| `attributes` | `[]` | → Attribute |
|
|
||||||
| `tabularSections` | `{}` | → TabularSection |
|
|
||||||
|
|
||||||
Модули: `Ext/ObjectModule.bsl`, `Ext/Flowchart.xml`.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "type": "BusinessProcess", "name": "Задание", "task": "Task.ЗадачаИсполнителя", "attributes": ["Описание: String(200)"] }
|
|
||||||
```
|
|
||||||
|
|
||||||
## Task
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `numberType` | `String` | NumberType |
|
|
||||||
| `numberLength` | `14` | NumberLength |
|
|
||||||
| `checkUnique` | `true` | CheckUnique |
|
|
||||||
| `autonumbering` | `true` | Autonumbering |
|
|
||||||
| `descriptionLength` | `150` | DescriptionLength |
|
|
||||||
| `addressing` | `""` | Addressing (ссылка на РС адресации) |
|
|
||||||
| `mainAddressingAttribute` | `""` | MainAddressingAttribute |
|
|
||||||
| `currentPerformer` | `""` | CurrentPerformer |
|
|
||||||
| `attributes` | `[]` | → Attribute |
|
|
||||||
| `tabularSections` | `{}` | → TabularSection |
|
|
||||||
| `addressingAttributes` | `[]` | → AddressingAttribute (shorthand или объект) |
|
|
||||||
|
|
||||||
AddressingAttribute — shorthand `"Имя: Тип"` или объект `{ "name", "type", "addressingDimension" }`.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "Task", "name": "ЗадачаИсполнителя",
|
|
||||||
"addressingAttributes": ["Исполнитель: CatalogRef.Пользователи"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## ExchangePlan
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `codeLength` | `9` | CodeLength |
|
|
||||||
| `descriptionLength` | `100` | DescriptionLength |
|
|
||||||
| `distributedInfoBase` | `false` | DistributedInfoBase |
|
|
||||||
| `attributes` | `[]` | → Attribute |
|
|
||||||
| `tabularSections` | `{}` | → TabularSection |
|
|
||||||
|
|
||||||
Модули: `Ext/ObjectModule.bsl`, `Ext/Content.xml`.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "type": "ExchangePlan", "name": "ОбменССайтом", "attributes": ["АдресСервера: String(200)"] }
|
|
||||||
```
|
|
||||||
|
|
||||||
## CommonModule
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `context` | — | Шорткат (см. ниже) |
|
|
||||||
| `global` | `false` | Global |
|
|
||||||
| `server` | `false` | Server |
|
|
||||||
| `serverCall` | `false` | ServerCall |
|
|
||||||
| `clientManagedApplication` | `false` | ClientManagedApplication |
|
|
||||||
| `externalConnection` | `false` | ExternalConnection |
|
|
||||||
| `privileged` | `false` | Privileged |
|
|
||||||
| `returnValuesReuse` | `DontUse` | ReturnValuesReuse |
|
|
||||||
|
|
||||||
Шорткаты `context`: `"server"` → Server+ServerCall, `"client"` → ClientManagedApplication, `"serverClient"` → Server+ClientManagedApplication.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "type": "CommonModule", "name": "ОбщиеФункции", "context": "serverClient" }
|
|
||||||
```
|
|
||||||
|
|
||||||
## ScheduledJob
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `methodName` | `""` | MethodName |
|
|
||||||
| `description` | = synonym | Description |
|
|
||||||
| `use` | `false` | Use |
|
|
||||||
| `predefined` | `false` | Predefined |
|
|
||||||
| `restartCountOnFailure` | `3` | RestartCountOnFailure |
|
|
||||||
| `restartIntervalOnFailure` | `10` | RestartIntervalOnFailure |
|
|
||||||
|
|
||||||
Формат `methodName`: `"МодульСервер.Процедура"` — авто-дополняется до `CommonModule.МодульСервер.Процедура`.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "type": "ScheduledJob", "name": "ОбменДанными", "methodName": "ОбменДаннымиСервер.Выполнить" }
|
|
||||||
```
|
|
||||||
|
|
||||||
## EventSubscription
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `source` | `[]` | Source (массив, формат `XxxObject.Name`) |
|
|
||||||
| `event` | `BeforeWrite` | Event |
|
|
||||||
| `handler` | `""` | Handler |
|
|
||||||
|
|
||||||
Формат `handler`: `"МодульСервер.Процедура"` — авто-дополняется до `CommonModule.МодульСервер.Процедура`.
|
|
||||||
|
|
||||||
Значения `event`: `BeforeWrite`, `OnWrite`, `BeforeDelete`, `OnReadAtServer`, `FillCheckProcessing`.
|
|
||||||
|
|
||||||
Формат `source`: `"CatalogObject.Xxx"`, `"DocumentObject.Xxx"`.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "type": "EventSubscription", "name": "ПередЗаписью", "source": ["CatalogObject.Контрагенты"], "event": "BeforeWrite", "handler": "ОбщиеФункции.ПередЗаписью" }
|
|
||||||
```
|
|
||||||
|
|
||||||
## DocumentJournal
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `registeredDocuments` | `[]` | RegisteredDocuments (массив `"Document.Xxx"`) |
|
|
||||||
| `columns` | `[]` | → Column |
|
|
||||||
|
|
||||||
Колонки — строка `"Имя"` или объект `{ "name", "synonym", "indexing": "Index"/"DontIndex", "references": ["Document.Xxx.Attribute.Yyy"] }`.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "DocumentJournal", "name": "Взаимодействия",
|
|
||||||
"registeredDocuments": ["Document.Встреча", "Document.Звонок"],
|
|
||||||
"columns": [{ "name": "Организация", "indexing": "Index", "references": ["Document.Встреча.Attribute.Организация"] }]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Зависимости
|
|
||||||
|
|
||||||
- **ScheduledJob/EventSubscription** — процедура-обработчик должна существовать в модуле (экспортная)
|
|
||||||
- **BusinessProcess** → `Task` (задача должна существовать)
|
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
# Регистры и планы: InformationRegister, AccumulationRegister, AccountingRegister, CalculationRegister, ChartOfAccounts, ChartOfCharacteristicTypes, ChartOfCalculationTypes
|
|
||||||
|
|
||||||
## Измерения и ресурсы (общее)
|
|
||||||
|
|
||||||
Синтаксис аналогичен реквизитам (shorthand `"Имя: Тип | флаги"`).
|
|
||||||
|
|
||||||
Флаги измерений: `master`, `mainFilter`, `denyIncomplete`, `useInTotals` (AccumulationRegister only, default `true`).
|
|
||||||
|
|
||||||
```json
|
|
||||||
"dimensions": ["Организация: CatalogRef.Организации | master, mainFilter, denyIncomplete"],
|
|
||||||
"resources": ["Сумма: Number(15,2)"]
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## InformationRegister
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `writeMode` | `Independent` | WriteMode |
|
|
||||||
| `periodicity` | `Nonperiodical` | InformationRegisterPeriodicity |
|
|
||||||
| `mainFilterOnPeriod` | авто* | MainFilterOnPeriod |
|
|
||||||
| `dimensions` | `[]` | → Dimension |
|
|
||||||
| `resources` | `[]` | → Resource |
|
|
||||||
| `attributes` | `[]` | → Attribute |
|
|
||||||
|
|
||||||
\* `mainFilterOnPeriod` = `true` если `periodicity` != `Nonperiodical`.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "InformationRegister", "name": "КурсыВалют", "periodicity": "Day",
|
|
||||||
"dimensions": ["Валюта: CatalogRef.Валюты | master, mainFilter, denyIncomplete"],
|
|
||||||
"resources": ["Курс: Number(15,4)", "Кратность: Number(10,0)"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## AccumulationRegister
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `registerType` | `Balance` | RegisterType (`Balance` / `Turnovers`) |
|
|
||||||
| `enableTotalsSplitting` | `true` | EnableTotalsSplitting |
|
|
||||||
| `dimensions` | `[]` | → Dimension |
|
|
||||||
| `resources` | `[]` | → Resource |
|
|
||||||
| `attributes` | `[]` | → Attribute |
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "AccumulationRegister", "name": "ОстаткиТоваров", "registerType": "Balance",
|
|
||||||
"dimensions": ["Номенклатура: CatalogRef.Номенклатура"],
|
|
||||||
"resources": ["Количество: Number(15,3)"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## AccountingRegister
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `chartOfAccounts` | `""` | ChartOfAccounts (**обязательная** ссылка на план счетов) |
|
|
||||||
| `correspondence` | `false` | Correspondence |
|
|
||||||
| `periodAdjustmentLength` | `0` | PeriodAdjustmentLength |
|
|
||||||
| `dimensions` | `[]` | → Dimension |
|
|
||||||
| `resources` | `[]` | → Resource |
|
|
||||||
| `attributes` | `[]` | → Attribute |
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "AccountingRegister", "name": "Хозрасчетный",
|
|
||||||
"chartOfAccounts": "ChartOfAccounts.Хозрасчетный",
|
|
||||||
"dimensions": ["Организация: CatalogRef.Организации"],
|
|
||||||
"resources": ["Сумма: Number(15,2)"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## CalculationRegister
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `chartOfCalculationTypes` | `""` | ChartOfCalculationTypes (**обязательная** ссылка на ПВР) |
|
|
||||||
| `periodicity` | `Month` | Periodicity |
|
|
||||||
| `actionPeriod` | `false` | ActionPeriod |
|
|
||||||
| `basePeriod` | `false` | BasePeriod |
|
|
||||||
| `schedule` | `""` | Schedule (ссылка на РС графиков) |
|
|
||||||
| `dimensions` | `[]` | → Dimension |
|
|
||||||
| `resources` | `[]` | → Resource |
|
|
||||||
| `attributes` | `[]` | → Attribute |
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "CalculationRegister", "name": "Начисления",
|
|
||||||
"chartOfCalculationTypes": "ChartOfCalculationTypes.Начисления",
|
|
||||||
"periodicity": "Month",
|
|
||||||
"dimensions": ["Сотрудник: CatalogRef.Сотрудники"],
|
|
||||||
"resources": ["Сумма: Number(15,2)"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ChartOfCharacteristicTypes
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `codeLength` | `9` | CodeLength |
|
|
||||||
| `descriptionLength` | `25` | DescriptionLength |
|
|
||||||
| `autonumbering` | `true` | Autonumbering |
|
|
||||||
| `checkUnique` | `false` | CheckUnique |
|
|
||||||
| `characteristicExtValues` | `""` | CharacteristicExtValues |
|
|
||||||
| `valueTypes` | авто* | Type (составной тип значений характеристик) |
|
|
||||||
| `hierarchical` | `false` | Hierarchical |
|
|
||||||
| `attributes` | `[]` | → Attribute |
|
|
||||||
| `tabularSections` | `{}` | → TabularSection |
|
|
||||||
|
|
||||||
\* По умолчанию: Boolean, String(100), Number(15,2), DateTime.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "ChartOfCharacteristicTypes", "name": "ВидыСубконто",
|
|
||||||
"valueTypes": ["CatalogRef.Номенклатура", "CatalogRef.Контрагенты", "Boolean", "String", "Number(15,2)"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## ChartOfAccounts
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `extDimensionTypes` | `""` | ExtDimensionTypes (ссылка на ПВХ) |
|
|
||||||
| `maxExtDimensionCount` | `3` | MaxExtDimensionCount |
|
|
||||||
| `codeMask` | `""` | CodeMask |
|
|
||||||
| `codeLength` | `8` | CodeLength |
|
|
||||||
| `descriptionLength` | `120` | DescriptionLength |
|
|
||||||
| `codeSeries` | `WholeChartOfAccounts` | CodeSeries |
|
|
||||||
| `autoOrderByCode` | `true` | AutoOrderByCode |
|
|
||||||
| `orderLength` | `5` | OrderLength |
|
|
||||||
| `hierarchical` | `false` | Hierarchical |
|
|
||||||
| `accountingFlags` | `[]` | → AccountingFlag (Boolean-тип, массив имён) |
|
|
||||||
| `extDimensionAccountingFlags` | `[]` | → ExtDimensionAccountingFlag (Boolean-тип, массив имён) |
|
|
||||||
| `attributes` | `[]` | → Attribute |
|
|
||||||
| `tabularSections` | `{}` | → TabularSection |
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "ChartOfAccounts", "name": "Хозрасчетный",
|
|
||||||
"extDimensionTypes": "ChartOfCharacteristicTypes.ВидыСубконто", "maxExtDimensionCount": 3,
|
|
||||||
"codeLength": 8, "codeMask": "@@@.@@.@",
|
|
||||||
"accountingFlags": ["Валютный", "Количественный"],
|
|
||||||
"extDimensionAccountingFlags": ["Суммовой", "Валютный"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## ChartOfCalculationTypes
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `codeLength` | `9` | CodeLength |
|
|
||||||
| `descriptionLength` | `25` | DescriptionLength |
|
|
||||||
| `autonumbering` | `true` | Autonumbering |
|
|
||||||
| `checkUnique` | `false` | CheckUnique |
|
|
||||||
| `dependenceOnCalculationTypes` | `DontUse` | DependenceOnCalculationTypes |
|
|
||||||
| `actionPeriodUse` | `false` | ActionPeriodUse |
|
|
||||||
| `attributes` | `[]` | → Attribute |
|
|
||||||
| `tabularSections` | `{}` | → TabularSection |
|
|
||||||
|
|
||||||
`dependenceOnCalculationTypes`: `DontUse`, `OnActionPeriod`.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "type": "ChartOfCalculationTypes", "name": "Начисления", "dependenceOnCalculationTypes": "OnActionPeriod" }
|
|
||||||
```
|
|
||||||
|
|
||||||
## Зависимости
|
|
||||||
|
|
||||||
- **AccountingRegister** требует `ChartOfAccounts` (и документ-регистратор)
|
|
||||||
- **CalculationRegister** требует `ChartOfCalculationTypes` (и документ-регистратор)
|
|
||||||
- **ChartOfAccounts** ссылается на `ChartOfCharacteristicTypes` через `extDimensionTypes`
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
# Веб-сервисы: HTTPService, WebService
|
|
||||||
|
|
||||||
## HTTPService
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `rootURL` | `= name.toLower()` | RootURL |
|
|
||||||
| `reuseSessions` | `DontUse` | ReuseSessions |
|
|
||||||
| `sessionMaxAge` | `20` | SessionMaxAge |
|
|
||||||
| `urlTemplates` | `{}` | → URLTemplate |
|
|
||||||
|
|
||||||
Модули: `Ext/Module.bsl`.
|
|
||||||
|
|
||||||
### urlTemplates — вложенная структура
|
|
||||||
|
|
||||||
`urlTemplates` — объект `{ "TemplateName": templateDef, ... }`.
|
|
||||||
|
|
||||||
Каждый `templateDef`:
|
|
||||||
- Строка — URL-шаблон: `"/v1/users"` (без методов)
|
|
||||||
- Объект:
|
|
||||||
|
|
||||||
| Поле | Умолчание | Описание |
|
|
||||||
|------|----------|----------|
|
|
||||||
| `template` | `"/templatename"` | URL-путь (с параметрами `{id}`) |
|
|
||||||
| `methods` | `{}` | Методы: `{ "MethodName": "HTTPMethod" }` |
|
|
||||||
|
|
||||||
Допустимые HTTPMethod: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS`, `CONNECT`, `TRACE`, `MERGE`.
|
|
||||||
|
|
||||||
Обработчик метода генерируется автоматически: `{TemplateName}{MethodName}` — должен быть реализован в `Ext/Module.bsl`.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "HTTPService", "name": "API", "rootURL": "api",
|
|
||||||
"urlTemplates": {
|
|
||||||
"Users": {
|
|
||||||
"template": "/v1/users/{id}",
|
|
||||||
"methods": { "Get": "GET", "Create": "POST", "Update": "PUT", "Delete": "DELETE" }
|
|
||||||
},
|
|
||||||
"Health": "/health"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## WebService
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `namespace` | `""` | Namespace (URI пространства имён WSDL) |
|
|
||||||
| `xdtoPackages` | `""` | XDTOPackages |
|
|
||||||
| `reuseSessions` | `DontUse` | ReuseSessions |
|
|
||||||
| `sessionMaxAge` | `20` | SessionMaxAge |
|
|
||||||
| `operations` | `{}` | → Operation |
|
|
||||||
|
|
||||||
Модули: `Ext/Module.bsl`.
|
|
||||||
|
|
||||||
### operations — вложенная структура
|
|
||||||
|
|
||||||
`operations` — объект `{ "OperationName": operationDef, ... }`.
|
|
||||||
|
|
||||||
Каждый `operationDef`:
|
|
||||||
- Строка — тип возврата: `"xs:boolean"` (параметров нет, обработчик = имя операции)
|
|
||||||
- Объект:
|
|
||||||
|
|
||||||
| Поле | Умолчание | Описание |
|
|
||||||
|------|----------|----------|
|
|
||||||
| `returnType` | `xs:string` | XDTO-тип возврата |
|
|
||||||
| `nillable` | `false` | Может ли вернуть null |
|
|
||||||
| `transactioned` | `false` | Выполнять в транзакции |
|
|
||||||
| `handler` | `= operationName` | Имя процедуры в модуле |
|
|
||||||
| `parameters` | `{}` | Параметры операции |
|
|
||||||
|
|
||||||
### parameters — параметры операции
|
|
||||||
|
|
||||||
`parameters` — объект `{ "ParamName": paramDef, ... }`.
|
|
||||||
|
|
||||||
Каждый `paramDef`:
|
|
||||||
- Строка — XDTO-тип: `"xs:string"` (direction = In, nillable = true)
|
|
||||||
- Объект:
|
|
||||||
|
|
||||||
| Поле | Умолчание | Описание |
|
|
||||||
|------|----------|----------|
|
|
||||||
| `type` | `xs:string` | XDTO-тип параметра |
|
|
||||||
| `nillable` | `true` | Может ли быть null |
|
|
||||||
| `direction` | `In` | Направление: `In`, `Out`, `InOut` |
|
|
||||||
|
|
||||||
Стандартные XDTO-типы: `xs:string`, `xs:boolean`, `xs:int`, `xs:long`, `xs:decimal`, `xs:dateTime`, `xs:base64Binary`.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "WebService", "name": "DataExchange",
|
|
||||||
"namespace": "http://www.1c.ru/DataExchange",
|
|
||||||
"operations": {
|
|
||||||
"TestConnection": {
|
|
||||||
"returnType": "xs:boolean",
|
|
||||||
"handler": "ПроверкаПодключения",
|
|
||||||
"parameters": {
|
|
||||||
"ErrorMessage": { "type": "xs:string", "direction": "Out" }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"GetVersion": "xs:string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# HTTPService, WebService (Веб-сервисы)
|
||||||
|
|
||||||
|
Модуль обоих — `Ext/Module.bsl`, в нём реализуются обработчики.
|
||||||
|
|
||||||
|
## HTTPService (HTTP-сервис)
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `rootURL` | `= name` (в нижнем регистре) | корневой URL |
|
||||||
|
| `reuseSessions` | `DontUse` | `DontUse` / `Use` / `AutoUse` |
|
||||||
|
| `sessionMaxAge` | `20` | время жизни сессии, сек |
|
||||||
|
| `urlTemplates` | `{}` | шаблоны URL (см. ниже) |
|
||||||
|
|
||||||
|
`urlTemplates` — объект `{ "ИмяШаблона": def }`, где `def`:
|
||||||
|
- строка — URL-путь без методов: `"/health"`;
|
||||||
|
- объект: `template` (путь с параметрами `{id}`, по умолчанию `/имяшаблона`), `synonym`, `comment`,
|
||||||
|
`methods` — `{ "ИмяМетода": def }`.
|
||||||
|
|
||||||
|
`methods` — значение либо строка (только HTTP-метод), либо объект: `httpMethod`, `handler`,
|
||||||
|
`synonym`, `comment`.
|
||||||
|
|
||||||
|
HTTP-методы: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS`, `CONNECT`, `TRACE`, `MERGE`.
|
||||||
|
Обработчик по умолчанию именуется `{ИмяШаблона}{ИмяМетода}`; в типовых конфигурациях он часто
|
||||||
|
произвольный — тогда задавайте `handler` явно.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "HTTPService", "name": "API", "rootURL": "api",
|
||||||
|
"urlTemplates": {
|
||||||
|
"Users": { "template": "/v1/users/{id}", "methods": { "Get": "GET", "Create": "POST", "Delete": "DELETE" } },
|
||||||
|
"Health": "/health"
|
||||||
|
} }
|
||||||
|
```
|
||||||
|
|
||||||
|
## WebService (Веб-сервис, SOAP)
|
||||||
|
|
||||||
|
| Ключ | Умолчание | Значения |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `namespace` | пусто | URI пространства имён WSDL |
|
||||||
|
| `xdtoPackages` | пусто | список пакетов (см. ниже) |
|
||||||
|
| `descriptorFileName` | `= name` + `.1cws` | имя файла дескриптора |
|
||||||
|
| `reuseSessions` | `DontUse` | `DontUse` / `Use` / `AutoUse` |
|
||||||
|
| `sessionMaxAge` | `20` | время жизни сессии, сек |
|
||||||
|
| `operations` | `{}` | операции (см. ниже) |
|
||||||
|
|
||||||
|
`xdtoPackages` — **массив** значений: `"XDTOPackage.Имя"` — пакет конфигурации, любое другое
|
||||||
|
значение — URI внешнего пространства имён (например `"http://v8.1c.ru/8.3/data/ext"`).
|
||||||
|
|
||||||
|
`operations` — объект `{ "ИмяОперации": def }`, где `def`:
|
||||||
|
- строка — XDTO-тип возврата без параметров: `"xs:string"`;
|
||||||
|
- объект: `returnType` (по умолчанию `xs:string`), `nillable` (bool), `transactioned` (bool),
|
||||||
|
`procedureName` (имя процедуры, по умолчанию = имя операции; синоним ключа — `handler`),
|
||||||
|
`dataLockControlMode` (по умолчанию `Managed`), `synonym`, `comment`, `parameters`.
|
||||||
|
|
||||||
|
`parameters` — объект `{ "ИмяПараметра": def }`, где `def`:
|
||||||
|
- строка — XDTO-тип (`direction` = `In`);
|
||||||
|
- объект: `type` (по умолчанию `xs:string`), `nillable` (bool, по умолчанию `true`),
|
||||||
|
`direction` (`In` / `Out` / `InOut`), `synonym`, `comment`.
|
||||||
|
|
||||||
|
XDTO-типы: `xs:string`, `xs:boolean`, `xs:int`, `xs:long`, `xs:decimal`, `xs:dateTime`, `xs:base64Binary`.
|
||||||
|
Тип из собственного пространства имён задаётся в нотации Кларка — `"{http://ваш.uri}ИмяТипа"`;
|
||||||
|
компилятор сам объявит локальный `xmlns` в теге, как это делает платформа.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "WebService", "name": "DataExchange", "namespace": "http://www.1c.ru/DataExchange",
|
||||||
|
"operations": {
|
||||||
|
"TestConnection": { "returnType": "xs:boolean", "handler": "ПроверкаПодключения",
|
||||||
|
"parameters": { "ErrorMessage": { "type": "xs:string", "direction": "Out" } } },
|
||||||
|
"GetVersion": "xs:string"
|
||||||
|
} }
|
||||||
|
```
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,52 @@
|
|||||||
|
---
|
||||||
|
name: meta-decompile
|
||||||
|
description: Декомпиляция объекта метаданных 1С в JSON-заготовку формата meta-compile. Используй когда нужно получить черновик DSL-описания нового объекта по образцу другого. Не сохраняет UUID/модули/формы.
|
||||||
|
argument-hint: <ObjectPath> [-OutputPath <out.json>]
|
||||||
|
disable-model-invocation: true
|
||||||
|
allowed-tools:
|
||||||
|
- Bash
|
||||||
|
- Read
|
||||||
|
- Write
|
||||||
|
- Glob
|
||||||
|
---
|
||||||
|
|
||||||
|
# /meta-decompile — DSL-заготовка из XML объекта метаданных
|
||||||
|
|
||||||
|
Читает XML объекта метаданных (`Catalogs/Имя.xml` и т.п.) и эмитит компактный JSON в формате `/meta-compile`. Назначение — **взять существующий объект образцом и собрать по нему НОВЫЙ**: декомпилировать → поправить → скомпилировать под другим именем.
|
||||||
|
|
||||||
|
## ⚠️ Главное: это НЕ обратимая выгрузка
|
||||||
|
|
||||||
|
Компиляция черновика создаёт **новый объект с новой идентичностью**, а не копию исходного. В JSON **не** попадают: UUID (идентичность самого объекта и всех дочерних), тела модулей, формы, макеты, права. Захватываются только структура и свойства.
|
||||||
|
|
||||||
|
Отсюда правило: **никогда не компилируй черновик поверх объекта-источника и не выдавай его за «реимпорт»** — у пересобранного объекта другие UUID, поэтому все ссылки на исходный объект (из кода, других объектов, состава подсистем, предопределённых данных) сломаются, а код модулей и формы пропадут.
|
||||||
|
|
||||||
|
## Когда использовать
|
||||||
|
|
||||||
|
**Собрать новый объект по образцу существующего** — получить DSL-заготовку рабочего объекта, переименовать и адаптировать состав, скомпилировать в новый. Быстрее, чем писать DSL с нуля для богатого объекта.
|
||||||
|
|
||||||
|
## Когда **не** использовать
|
||||||
|
|
||||||
|
- **Точечная правка существующего объекта** (добавить реквизит, ТЧ, свойство) → `/meta-edit`. Цикл decompile→compile тут вреден: даёт объект с новой идентичностью и теряет модули/формы.
|
||||||
|
- **Сохранить / восстановить / перенести тот же объект** (бэкап, миграция между конфигурациями с сохранением ссылок) → штатная выгрузка 1С (`/db-dump-xml` ↔ `/db-load-xml`, CF), а не decompile.
|
||||||
|
- **Просто понять структуру** объекта (реквизиты, ТЧ, типы) без пересборки → `/meta-info` (дешевле, не плодит файл).
|
||||||
|
|
||||||
|
## Параметры
|
||||||
|
|
||||||
|
| Параметр | Описание |
|
||||||
|
|----------|----------|
|
||||||
|
| `ObjectPath` | Путь к XML объекта (`Catalogs/Имя.xml`), обязательный |
|
||||||
|
| `OutputPath` | Путь к выходному JSON. Если не задан — JSON в stdout |
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-decompile.ps1" -ObjectPath "<Объект.xml>" -OutputPath "<out.json>"
|
||||||
|
```
|
||||||
|
|
||||||
|
Неподдерживаемый тип объекта или не-`MetaDataObject` root → ненулевой код выхода и сообщение в stderr.
|
||||||
|
|
||||||
|
## Workflow (сборка нового объекта по образцу)
|
||||||
|
|
||||||
|
1. `/meta-decompile <Образец.xml> -OutputPath draft.json` — получить заготовку.
|
||||||
|
2. В `draft.json` **сменить `name`** на имя нового объекта и адаптировать состав (реквизиты/ТЧ/свойства). Ссылки на *другие* объекты (владельцы, ввод на основании, типы) — по имени, сохраняются как есть.
|
||||||
|
3. `/meta-compile -JsonPath draft.json -OutputDir <ConfigDir>` — собрать (объект получит свежие UUID).
|
||||||
|
4. `/meta-validate` + `/meta-info` — проверить.
|
||||||
|
5. Модули, формы, макеты, права — добавить отдельно (в черновик они не попадают).
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -59,6 +59,9 @@ Batch через `;;` во всех операциях. Подробный си
|
|||||||
|
|
||||||
Позиционная вставка: `"Склад: CatalogRef.Склады >> after Организация"`.
|
Позиционная вставка: `"Склад: CatalogRef.Склады >> after Организация"`.
|
||||||
|
|
||||||
|
`modify-attribute` умеет и структурные свойства реквизита — формат/подсказку, форму и параметры выбора,
|
||||||
|
значение заполнения, границы (`Format`, `ChoiceForm`, `ChoiceParameters`, `FillValue`, `MinValue`/`MaxValue` и др.).
|
||||||
|
|
||||||
### Свойства объекта — [properties-reference.md](properties-reference.md)
|
### Свойства объекта — [properties-reference.md](properties-reference.md)
|
||||||
|
|
||||||
| Операция | Формат Value | Пример |
|
| Операция | Формат Value | Пример |
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user