Compare commits

..
Author SHA1 Message Date
github-actions[bot] 063d4e98fc Auto-build: claude-code (python) from ccd860a 2026-08-04 11:25:39 +00:00
3110 changed files with 16800 additions and 196650 deletions
-32
View File
@@ -1,32 +0,0 @@
{
"name": "cc-1c-skills",
"interface": {
"displayName": "1C Skills"
},
"plugins": [
{
"name": "1c-skills",
"source": {
"source": "url",
"url": "https://github.com/Nikolay-Shirokov/cc-1c-skills.git",
"ref": "port-codex"
},
"policy": {
"installation": "AVAILABLE"
},
"category": "Development"
},
{
"name": "1c-skills-py",
"source": {
"source": "url",
"url": "https://github.com/Nikolay-Shirokov/cc-1c-skills.git",
"ref": "port-codex-py"
},
"policy": {
"installation": "AVAILABLE"
},
"category": "Development"
}
]
}
-24
View File
@@ -1,24 +0,0 @@
{
"$schema": "https://json.schemastore.org/claude-code-marketplace-manifest.json",
"name": "cc-1c-skills",
"description": "Маркетплейс навыков для разработки на платформе 1С:Предприятие",
"owner": {
"name": "Nikolay Shirokov"
},
"plugins": [
{
"name": "1c-skills",
"source": "./",
"description": "[PowerShell] Навыки для разработки на 1С:Предприятие 8.3 — абстракции над XML-форматами и CLI конфигуратора, плюс глаза и руки для тестирования через веб-клиент."
},
{
"name": "1c-skills-py",
"source": {
"source": "github",
"repo": "Nikolay-Shirokov/cc-1c-skills",
"ref": "port-claude-code-py"
},
"description": "[Python] То же — для Linux/Mac или когда PowerShell недоступен."
}
]
}
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "1c-skills", "name": "1c-skills-py",
"description": "[PowerShell] Навыки для разработки на 1С:Предприятие 8.3 — абстракции над XML-форматами и CLI конфигуратора, плюс глаза и руки для тестирования через веб-клиент.", "description": "[Python] Навыки для разработки на 1С:Предприятие 8.3 — абстракции над XML-форматами и CLI конфигуратора, плюс глаза и руки для тестирования через веб-клиент. Linux/Mac или когда PowerShell недоступен.",
"author": { "author": {
"name": "Nikolay Shirokov" "name": "Nikolay Shirokov"
}, },
+1 -1
View File
@@ -24,7 +24,7 @@ allowed-tools:
| `NoValidate` | Пропустить авто-валидацию | | `NoValidate` | Пропустить авто-валидацию |
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-edit.ps1" -ConfigPath '<path>' -Operation modify-property -Value 'Version=1.0.0.1' python "${CLAUDE_SKILL_DIR}/scripts/cf-edit.py" -ConfigPath '<path>' -Operation modify-property -Value 'Version=1.0.0.1'
``` ```
## Операции ## Операции
+7 -2
View File
@@ -1,4 +1,4 @@
# cf-edit v1.10 — 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,
@@ -167,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
@@ -864,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
+6 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# cf-edit v1.10 — 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
@@ -410,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
@@ -959,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 -1
View File
@@ -23,7 +23,7 @@ allowed-tools:
| `OutFile` | Записать результат в файл (UTF-8 BOM) | | `OutFile` | Записать результат в файл (UTF-8 BOM) |
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-info.ps1" -ConfigPath "<путь>" python "${CLAUDE_SKILL_DIR}/scripts/cf-info.py" -ConfigPath "<путь>"
``` ```
## Три режима ## Три режима
+1 -1
View File
@@ -24,7 +24,7 @@ allowed-tools:
| `CompatibilityMode` | Режим совместимости (default: `Version8_3_24`) | | `CompatibilityMode` | Режим совместимости (default: `Version8_3_24`) |
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-init.ps1" -Name "МояКонфигурация" python "${CLAUDE_SKILL_DIR}/scripts/cf-init.py" -Name "МояКонфигурация"
``` ```
## Примеры ## Примеры
+9 -4
View File
@@ -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>
+8 -3
View File
@@ -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>
+2 -2
View File
@@ -24,6 +24,6 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-validate.ps1" -ConfigPath "upload/cfempty" python "${CLAUDE_SKILL_DIR}/scripts/cf-validate.py" -ConfigPath "upload/cfempty"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-validate.ps1" -ConfigPath "upload/cfempty/Configuration.xml" python "${CLAUDE_SKILL_DIR}/scripts/cf-validate.py" -ConfigPath "upload/cfempty/Configuration.xml"
``` ```
@@ -1,4 +1,4 @@
# cf-validate v1.4 — 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)]
@@ -205,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.4 — 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
@@ -232,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 -1
View File
@@ -71,7 +71,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-borrow.ps1" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты" python "${CLAUDE_SKILL_DIR}/scripts/cfe-borrow.py" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты"
``` ```
## Примеры ## Примеры
@@ -1,4 +1,4 @@
# cfe-borrow v1.9 — 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.9 — 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"]
@@ -536,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):
@@ -612,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 -1
View File
@@ -23,7 +23,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-diff.ps1" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A python "${CLAUDE_SKILL_DIR}/scripts/cfe-diff.py" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A
``` ```
## Mode A — обзор расширения ## Mode A — обзор расширения
+1 -1
View File
@@ -44,7 +44,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-init.ps1" -Name "МоёРасширение" python "${CLAUDE_SKILL_DIR}/scripts/cfe-init.py" -Name "МоёРасширение"
``` ```
## Примеры ## Примеры
+1 -1
View File
@@ -110,7 +110,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-patch-method.ps1" -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before python "${CLAUDE_SKILL_DIR}/scripts/cfe-patch-method.py" -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
``` ```
## Примеры ## Примеры
+2 -2
View File
@@ -24,6 +24,6 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src" python "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.py" -ExtensionPath "src"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src/Configuration.xml" python "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.py" -ExtensionPath "src/Configuration.xml"
``` ```
@@ -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
@@ -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
+7 -5
View File
@@ -31,7 +31,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" <параметры> python "${CLAUDE_SKILL_DIR}/scripts/db-create.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -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`
@@ -57,14 +59,14 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" <п
```powershell ```powershell
# Создать файловую базу # Создать файловую базу
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" python "${CLAUDE_SKILL_DIR}/scripts/db-create.py" -InfoBasePath "C:\Bases\NewDB"
# Создать серверную базу # Создать серверную базу
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" python "${CLAUDE_SKILL_DIR}/scripts/db-create.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test"
# Создать из шаблона CF # Создать из шаблона CF
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf" python "${CLAUDE_SKILL_DIR}/scripts/db-create.py" -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf"
# Создать и добавить в список баз # Создать и добавить в список баз
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" -AddToList -ListName "Новая база" python "${CLAUDE_SKILL_DIR}/scripts/db-create.py" -InfoBasePath "C:\Bases\NewDB" -AddToList -ListName "Новая база"
``` ```
+229 -20
View File
@@ -1,4 +1,4 @@
# db-create v1.7 — 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 не исполняется. # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
@@ -30,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"
@@ -61,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
@@ -111,32 +268,75 @@ if (-not (Test-Path $V8Path)) {
} }
# --- Detect engine (ibcmd vs 1cv8) by exe name --- # --- Detect engine (ibcmd vs 1cv8) by exe name ---
function Invoke-IbcmdProcess { function ConvertFrom-PlatformBytes {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt # ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's # OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process. # one of them outright mangles Cyrillic.
param([string]$Exe, [string[]]$IbArgs) 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 = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe $psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' ' $psi.Arguments = if ($PreQuoted) {
$ProcArgs -join ' '
} else {
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
}
$psi.UseShellExecute = $false $psi.UseShellExecute = $false
$psi.CreateNoWindow = $true $psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true $psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $true $psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true $psi.RedirectStandardError = $true
try {
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
} catch {}
$p = [System.Diagnostics.Process]::Start($psi) $p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close() $p.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd() # stderr is drained in parallel: reading the streams one after another deadlocks
$err = $p.StandardError.ReadToEnd() # 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() $p.WaitForExit()
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
if ($err) { $out += $err } if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode } 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 { function Test-FileIbCreated {
# File-infobase postcondition: the platform must have produced a non-empty 1Cv8.1CD. # File-infobase postcondition: the platform must have produced a non-empty 1Cv8.1CD.
@@ -148,6 +348,10 @@ function Test-FileIbCreated {
$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) {
@@ -181,8 +385,9 @@ try {
} }
} }
$arguments += "--data=$tempDir" $arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $($arguments -join ' ')" $arguments += $extraArgs
$__ib = Invoke-IbcmdProcess $V8Path $arguments Write-Host "Running: ibcmd $((Format-ArgsForDisplay $arguments $engine) -join ' ')"
$__ib = Invoke-PlatformProcess $V8Path $arguments
$output = $__ib.Output $output = $__ib.Output
$exitCode = $__ib.ExitCode $exitCode = $__ib.ExitCode
$ibMissing = ($exitCode -eq 0) -and -not (Test-FileIbCreated $InfoBasePath) $ibMissing = ($exitCode -eq 0) -and -not (Test-FileIbCreated $InfoBasePath)
@@ -194,7 +399,7 @@ try {
} 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
} }
@@ -202,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 {
@@ -226,11 +433,12 @@ 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. # Postcondition (file infobase only): exit 0 without a non-empty 1Cv8.1CD is a false success.
@@ -257,6 +465,7 @@ try {
Write-Host "--- End ---" Write-Host "--- End ---"
} }
} }
Write-PlatformOutput $__v8.Output
exit $exitCode exit $exitCode
+268 -21
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-create v1.7 — 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,6 +36,163 @@ 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): def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8).""" """Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p) parent = os.path.dirname(p)
@@ -93,6 +250,18 @@ IBCMD_NOUSER_HINT = (
) )
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): def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively. """Run an ibcmd command non-interactively.
@@ -103,7 +272,67 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
if warn_no_user and os.name == "nt" and not has_username: if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT) sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush() sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace") 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():
@@ -120,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:
@@ -150,7 +401,8 @@ 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)
print(f"Running: ibcmd {' '.join(format_args_for_display(arguments, engine))}")
result = run_ibcmd([v8path] + arguments, warn_no_user=False) result = run_ibcmd([v8path] + arguments, warn_no_user=False)
exit_code = result.returncode exit_code = result.returncode
ib_missing = exit_code == 0 and not file_ib_created(args.InfoBasePath) ib_missing = exit_code == 0 and not file_ib_created(args.InfoBasePath)
@@ -166,10 +418,7 @@ def main():
) )
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)
if result.stdout: print_platform_output(result)
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
sys.exit(exit_code) sys.exit(exit_code)
# --- Temp dir --- # --- Temp dir ---
@@ -180,36 +429,33 @@ 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 ---
@@ -243,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)
+6 -4
View File
@@ -35,7 +35,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" <параметры> python "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -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`
@@ -58,11 +60,11 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" <п
```powershell ```powershell
# Выгрузка конфигурации (файловая база) # Выгрузка конфигурации (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\config.cf" python "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\config.cf"
# Серверная база # Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "config.cf" python "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "config.cf"
# Выгрузка расширения # Выгрузка расширения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "ext.cfe" -Extension "МоёРасширение" python "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "ext.cfe" -Extension "МоёРасширение"
``` ```
+240 -20
View File
@@ -1,4 +1,4 @@
# db-dump-cf v1.9 — 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 не исполняется. # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
@@ -36,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"
@@ -70,7 +76,13 @@ 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
@@ -83,6 +95,164 @@ function Protect-Secrets {
return $Text 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
@@ -127,32 +297,75 @@ if (-not (Test-Path $V8Path)) {
} }
# --- Detect engine (ibcmd vs 1cv8) by exe name --- # --- Detect engine (ibcmd vs 1cv8) by exe name ---
function Invoke-IbcmdProcess { function ConvertFrom-PlatformBytes {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt # ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's # OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process. # one of them outright mangles Cyrillic.
param([string]$Exe, [string[]]$IbArgs) 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 = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe $psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' ' $psi.Arguments = if ($PreQuoted) {
$ProcArgs -join ' '
} else {
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
}
$psi.UseShellExecute = $false $psi.UseShellExecute = $false
$psi.CreateNoWindow = $true $psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true $psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $true $psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true $psi.RedirectStandardError = $true
try {
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
} catch {}
$p = [System.Diagnostics.Process]::Start($psi) $p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close() $p.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd() # stderr is drained in parallel: reading the streams one after another deadlocks
$err = $p.StandardError.ReadToEnd() # 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() $p.WaitForExit()
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
if ($err) { $out += $err } if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode } 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 { function Test-OutputNonEmpty {
# Postcondition: the platform must have produced a non-empty output file. # Postcondition: the platform must have produced a non-empty output file.
@@ -163,6 +376,10 @@ function Test-OutputNonEmpty {
$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) {
@@ -197,8 +414,9 @@ 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 $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))" $arguments += $extraArgs
$__ib = Invoke-IbcmdProcess $V8Path $arguments Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__ib = Invoke-PlatformProcess $V8Path $arguments
$output = $__ib.Output $output = $__ib.Output
$exitCode = $__ib.ExitCode $exitCode = $__ib.ExitCode
$outMissing = ($exitCode -eq 0) -and -not (Test-OutputNonEmpty $OutputFile) $outMissing = ($exitCode -eq 0) -and -not (Test-OutputNonEmpty $OutputFile)
@@ -210,7 +428,7 @@ try {
} 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
} }
@@ -240,11 +458,12 @@ 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 $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))" 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. # Postcondition: exit 0 without a non-empty output file is a false success.
@@ -266,6 +485,7 @@ try {
Write-Host "--- End ---" Write-Host "--- End ---"
} }
} }
Write-PlatformOutput $__v8.Output
exit $exitCode exit $exitCode
+278 -21
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-dump-cf v1.9 — 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,6 +36,163 @@ 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): def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8).""" """Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p) parent = os.path.dirname(p)
@@ -86,6 +243,85 @@ IBCMD_NOUSER_HINT = (
) )
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): def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively. """Run an ibcmd command non-interactively.
@@ -96,7 +332,10 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
if warn_no_user and os.name == "nt" and not has_username: if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT) sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush() sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace") 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): def output_nonempty(path):
@@ -130,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:
@@ -165,7 +427,8 @@ 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 {_redact(' '.join(arguments), args.Password, args.UserName)}") arguments.extend(extra_args)
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_ibcmd([v8path] + arguments, bool(args.UserName)) result = run_ibcmd([v8path] + arguments, bool(args.UserName))
exit_code = result.returncode exit_code = result.returncode
out_missing = exit_code == 0 and not output_nonempty(args.OutputFile) out_missing = exit_code == 0 and not output_nonempty(args.OutputFile)
@@ -177,10 +440,6 @@ def main():
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — configuration was not dumped", file=sys.stderr) print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — configuration was not dumped", 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)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
sys.exit(exit_code) sys.exit(exit_code)
# --- Temp dir --- # --- Temp dir ---
@@ -192,35 +451,32 @@ 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 {_redact(' '.join(arguments), args.Password, args.UserName)}") 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 ---
@@ -246,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:
+5 -3
View File
@@ -38,7 +38,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" <параметры> python "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -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`
@@ -59,10 +61,10 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" <п
```powershell ```powershell
# Выгрузка ИБ (файловая база) # Выгрузка ИБ (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\base.dt" python "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\base.dt"
# Серверная база # Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "base.dt" python "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "base.dt"
``` ```
## Связанные навыки ## Связанные навыки
+241 -20
View File
@@ -1,4 +1,4 @@
# db-dump-dt v1.8 — 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 не исполняется. # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
@@ -29,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"
#> #>
@@ -54,7 +60,13 @@ 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
@@ -67,6 +79,164 @@ function Protect-Secrets {
return $Text 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
@@ -111,32 +281,75 @@ if (-not (Test-Path $V8Path)) {
} }
# --- Detect engine (ibcmd vs 1cv8) by exe name --- # --- Detect engine (ibcmd vs 1cv8) by exe name ---
function Invoke-IbcmdProcess { function ConvertFrom-PlatformBytes {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt # ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's # OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process. # one of them outright mangles Cyrillic.
param([string]$Exe, [string[]]$IbArgs) 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 = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe $psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' ' $psi.Arguments = if ($PreQuoted) {
$ProcArgs -join ' '
} else {
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
}
$psi.UseShellExecute = $false $psi.UseShellExecute = $false
$psi.CreateNoWindow = $true $psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true $psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $true $psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true $psi.RedirectStandardError = $true
try {
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
} catch {}
$p = [System.Diagnostics.Process]::Start($psi) $p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close() $p.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd() # stderr is drained in parallel: reading the streams one after another deadlocks
$err = $p.StandardError.ReadToEnd() # 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() $p.WaitForExit()
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
if ($err) { $out += $err } if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode } 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 { function Test-OutputNonEmpty {
# Postcondition: the platform must have produced a non-empty output file. # Postcondition: the platform must have produced a non-empty output file.
@@ -147,6 +360,10 @@ function Test-OutputNonEmpty {
$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) {
@@ -177,8 +394,10 @@ try {
$arguments += "$OutputFile" $arguments += "$OutputFile"
$arguments += "--data=$tempDir" $arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
$__ib = Invoke-IbcmdProcess $V8Path $arguments $arguments += $extraArgs
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__ib = Invoke-PlatformProcess $V8Path $arguments
$output = $__ib.Output $output = $__ib.Output
$exitCode = $__ib.ExitCode $exitCode = $__ib.ExitCode
$outMissing = ($exitCode -eq 0) -and -not (Test-OutputNonEmpty $OutputFile) $outMissing = ($exitCode -eq 0) -and -not (Test-OutputNonEmpty $OutputFile)
@@ -190,7 +409,7 @@ try {
} 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
} }
@@ -213,11 +432,12 @@ 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 $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))" 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. # Postcondition: exit 0 without a non-empty output file is a false success.
@@ -239,6 +459,7 @@ try {
Write-Host "--- End ---" Write-Host "--- End ---"
} }
} }
Write-PlatformOutput $__v8.Output
exit $exitCode exit $exitCode
+277 -20
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-dump-dt v1.8 — 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,6 +36,163 @@ 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): def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8).""" """Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p) parent = os.path.dirname(p)
@@ -86,6 +243,85 @@ IBCMD_NOUSER_HINT = (
) )
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): def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively. """Run an ibcmd command non-interactively.
@@ -96,7 +332,10 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
if warn_no_user and os.name == "nt" and not has_username: if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT) sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush() sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace") 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): def output_nonempty(path):
@@ -128,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:
@@ -158,7 +420,8 @@ 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 {_redact(' '.join(arguments), args.Password, args.UserName)}") arguments.extend(extra_args)
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_ibcmd([v8path] + arguments, bool(args.UserName)) result = run_ibcmd([v8path] + arguments, bool(args.UserName))
exit_code = result.returncode exit_code = result.returncode
out_missing = exit_code == 0 and not output_nonempty(args.OutputFile) out_missing = exit_code == 0 and not output_nonempty(args.OutputFile)
@@ -170,10 +433,6 @@ def main():
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — information base was not dumped", file=sys.stderr) print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — information base was not dumped", 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)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
sys.exit(exit_code) sys.exit(exit_code)
# --- Temp dir --- # --- Temp dir ---
@@ -185,29 +444,26 @@ 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 {_redact(' '.join(arguments), args.Password, args.UserName)}") 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 ---
@@ -233,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:
+8 -6
View File
@@ -37,7 +37,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" <параметры> python "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -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`
@@ -74,17 +76,17 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" <
```powershell ```powershell
# Полная выгрузка (файловая база) # Полная выгрузка (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full python "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.py" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full
# Инкрементальная выгрузка # Инкрементальная выгрузка
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Changes python "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Changes
# Частичная выгрузка # Частичная выгрузка
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ" python "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ"
# Серверная база # Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -ConfigDir "C:\WS\cfsrc" -Mode Full python "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -ConfigDir "C:\WS\cfsrc" -Mode Full
# Выгрузка расширения # Выгрузка расширения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение" python "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
``` ```
@@ -1,4 +1,4 @@
# db-dump-xml v1.11 — 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 не исполняется. # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
@@ -48,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
@@ -93,7 +99,13 @@ 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
@@ -106,6 +118,164 @@ function Protect-Secrets {
return $Text 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
@@ -150,32 +320,75 @@ if (-not (Test-Path $V8Path)) {
} }
# --- Detect engine (ibcmd vs 1cv8) by exe name --- # --- Detect engine (ibcmd vs 1cv8) by exe name ---
function Invoke-IbcmdProcess { function ConvertFrom-PlatformBytes {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt # ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's # OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process. # one of them outright mangles Cyrillic.
param([string]$Exe, [string[]]$IbArgs) 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 = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe $psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' ' $psi.Arguments = if ($PreQuoted) {
$ProcArgs -join ' '
} else {
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
}
$psi.UseShellExecute = $false $psi.UseShellExecute = $false
$psi.CreateNoWindow = $true $psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true $psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $true $psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true $psi.RedirectStandardError = $true
try {
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
} catch {}
$p = [System.Diagnostics.Process]::Start($psi) $p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close() $p.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd() # stderr is drained in parallel: reading the streams one after another deadlocks
$err = $p.StandardError.ReadToEnd() # 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() $p.WaitForExit()
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
if ($err) { $out += $err } if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode } 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 { function Test-DirNonEmpty {
# Postcondition: the platform must have written files into the output directory. # Postcondition: the platform must have written files into the output directory.
@@ -186,6 +399,10 @@ function Test-DirNonEmpty {
$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) {
@@ -238,8 +455,9 @@ 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 $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))" $arguments += $extraArgs
$__ib = Invoke-IbcmdProcess $V8Path $arguments Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__ib = Invoke-PlatformProcess $V8Path $arguments
$output = $__ib.Output $output = $__ib.Output
$exitCode = $__ib.ExitCode $exitCode = $__ib.ExitCode
$outMissing = ($exitCode -eq 0) -and -not (Test-DirNonEmpty $ConfigDir) $outMissing = ($exitCode -eq 0) -and -not (Test-DirNonEmpty $ConfigDir)
@@ -251,7 +469,7 @@ try {
} 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
} }
@@ -309,11 +527,12 @@ 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 $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))" 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. # Postcondition: exit 0 with an empty output directory is a false success.
@@ -336,6 +555,7 @@ try {
Write-Host "--- End ---" Write-Host "--- End ---"
} }
} }
Write-PlatformOutput $__v8.Output
exit $exitCode exit $exitCode
+279 -22
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-dump-xml v1.11 — 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,6 +36,163 @@ 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): def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8).""" """Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p) parent = os.path.dirname(p)
@@ -86,6 +243,85 @@ IBCMD_NOUSER_HINT = (
) )
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): def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively. """Run an ibcmd command non-interactively.
@@ -96,7 +332,10 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
if warn_no_user and os.name == "nt" and not has_username: if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT) sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush() sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace") 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): def dir_nonempty(path):
@@ -143,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:
@@ -196,7 +458,8 @@ 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 {_redact(' '.join(arguments), args.Password, args.UserName)}") arguments.extend(extra_args)
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_ibcmd([v8path] + arguments, bool(args.UserName)) result = run_ibcmd([v8path] + arguments, bool(args.UserName))
exit_code = result.returncode exit_code = result.returncode
out_missing = exit_code == 0 and not dir_nonempty(args.ConfigDir) out_missing = exit_code == 0 and not dir_nonempty(args.ConfigDir)
@@ -208,10 +471,6 @@ def main():
print(f"Error: exit code 0 but no files under {args.ConfigDir} — configuration was not exported", file=sys.stderr) print(f"Error: exit code 0 but no files under {args.ConfigDir} — configuration was not exported", file=sys.stderr)
else: else:
print(f"Error exporting configuration (code: {exit_code})", file=sys.stderr) print(f"Error exporting configuration (code: {exit_code})", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
sys.exit(exit_code) sys.exit(exit_code)
# --- Temp dir --- # --- Temp dir ---
@@ -223,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":
@@ -249,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}")
@@ -259,22 +518,19 @@ 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 {_redact(' '.join(arguments), args.Password, args.UserName)}") 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 ---
@@ -301,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:
+3
View File
@@ -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 базы по умолчанию |
+6 -4
View File
@@ -36,7 +36,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" <параметры> python "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -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`
@@ -63,11 +65,11 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" <п
```powershell ```powershell
# Файловая база # Файловая база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\config.cf" python "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\config.cf"
# Серверная база # Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "config.cf" python "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "config.cf"
# Загрузка расширения # Загрузка расширения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "ext.cfe" -Extension "МоёРасширение" python "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "ext.cfe" -Extension "МоёРасширение"
``` ```
+240 -20
View File
@@ -1,4 +1,4 @@
# db-load-cf v1.10 — 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 не исполняется. # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
@@ -36,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"
@@ -70,7 +76,13 @@ 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
@@ -100,6 +112,164 @@ function Get-ExitAnnotation {
return "" 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
@@ -144,35 +314,82 @@ if (-not (Test-Path $V8Path)) {
} }
# --- Detect engine (ibcmd vs 1cv8) by exe name --- # --- Detect engine (ibcmd vs 1cv8) by exe name ---
function Invoke-IbcmdProcess { function ConvertFrom-PlatformBytes {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt # ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's # OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process. # one of them outright mangles Cyrillic.
param([string]$Exe, [string[]]$IbArgs) 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 = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe $psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' ' $psi.Arguments = if ($PreQuoted) {
$ProcArgs -join ' '
} else {
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
}
$psi.UseShellExecute = $false $psi.UseShellExecute = $false
$psi.CreateNoWindow = $true $psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true $psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $true $psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true $psi.RedirectStandardError = $true
try {
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
} catch {}
$p = [System.Diagnostics.Process]::Start($psi) $p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close() $p.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd() # stderr is drained in parallel: reading the streams one after another deadlocks
$err = $p.StandardError.ReadToEnd() # 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() $p.WaitForExit()
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
if ($err) { $out += $err } if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode } 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) {
@@ -207,8 +424,9 @@ 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 $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))" $arguments += $extraArgs
$__ib = Invoke-IbcmdProcess $V8Path $arguments Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__ib = Invoke-PlatformProcess $V8Path $arguments
$output = $__ib.Output $output = $__ib.Output
$exitCode = $__ib.ExitCode $exitCode = $__ib.ExitCode
if ($exitCode -eq 0) { if ($exitCode -eq 0) {
@@ -216,7 +434,7 @@ try {
} else { } else {
Write-Host "Error loading configuration (code: $exitCode)$(Get-ExitAnnotation $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
} }
@@ -246,11 +464,12 @@ 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 $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))" 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) {
@@ -267,6 +486,7 @@ try {
Write-Host "--- End ---" Write-Host "--- End ---"
} }
} }
Write-PlatformOutput $__v8.Output
exit $exitCode exit $exitCode
+278 -21
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-load-cf v1.10 — 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,6 +36,163 @@ 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): def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8).""" """Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p) parent = os.path.dirname(p)
@@ -86,6 +243,85 @@ IBCMD_NOUSER_HINT = (
) )
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): def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively. """Run an ibcmd command non-interactively.
@@ -96,7 +332,10 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
if warn_no_user and os.name == "nt" and not has_username: if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT) sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush() sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace") 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): def describe_exit(code):
@@ -148,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:
@@ -183,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 {_redact(' '.join(arguments), args.Password, args.UserName)}") arguments.extend(extra_args)
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_ibcmd([v8path] + arguments, bool(args.UserName)) result = run_ibcmd([v8path] + arguments, bool(args.UserName))
if result.returncode == 0: if result.returncode == 0:
print(f"Configuration loaded successfully from: {args.InputFile}") print(f"Configuration loaded successfully from: {args.InputFile}")
else: else:
print(f"Error loading configuration (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr) print(f"Error loading configuration (code: {result.returncode}){describe_exit(result.returncode)}", 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 ---
@@ -204,35 +463,32 @@ 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 {_redact(' '.join(arguments), args.Password, args.UserName)}") 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 ---
@@ -252,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:
+5 -3
View File
@@ -52,7 +52,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" <параметры> python "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -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`
@@ -80,10 +82,10 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" <п
```powershell ```powershell
# Файловая база # Файловая база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\base.dt" python "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\base.dt"
# Серверная база с ускорением загрузки # Серверная база с ускорением загрузки
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "base.dt" -JobsCount 4 python "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "base.dt" -JobsCount 4
``` ```
## Связанные навыки ## Связанные навыки
+241 -20
View File
@@ -1,4 +1,4 @@
# db-load-dt v1.9 — 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 не исполняется. # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
@@ -36,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"
#> #>
@@ -67,7 +73,13 @@ 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
@@ -97,6 +109,164 @@ function Get-ExitAnnotation {
return "" 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
@@ -141,35 +311,82 @@ if (-not (Test-Path $V8Path)) {
} }
# --- Detect engine (ibcmd vs 1cv8) by exe name --- # --- Detect engine (ibcmd vs 1cv8) by exe name ---
function Invoke-IbcmdProcess { function ConvertFrom-PlatformBytes {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt # ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's # OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process. # one of them outright mangles Cyrillic.
param([string]$Exe, [string[]]$IbArgs) 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 = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe $psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' ' $psi.Arguments = if ($PreQuoted) {
$ProcArgs -join ' '
} else {
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
}
$psi.UseShellExecute = $false $psi.UseShellExecute = $false
$psi.CreateNoWindow = $true $psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true $psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $true $psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true $psi.RedirectStandardError = $true
try {
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
} catch {}
$p = [System.Diagnostics.Process]::Start($psi) $p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close() $p.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd() # stderr is drained in parallel: reading the streams one after another deadlocks
$err = $p.StandardError.ReadToEnd() # 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() $p.WaitForExit()
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
if ($err) { $out += $err } if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode } 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) {
@@ -201,8 +418,10 @@ try {
$arguments += "$InputFile" $arguments += "$InputFile"
$arguments += "--data=$tempDir" $arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
$__ib = Invoke-IbcmdProcess $V8Path $arguments $arguments += $extraArgs
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__ib = Invoke-PlatformProcess $V8Path $arguments
$output = $__ib.Output $output = $__ib.Output
$exitCode = $__ib.ExitCode $exitCode = $__ib.ExitCode
if ($exitCode -eq 0) { if ($exitCode -eq 0) {
@@ -210,7 +429,7 @@ try {
} else { } else {
Write-Host "Error restoring information base (code: $exitCode)$(Get-ExitAnnotation $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
} }
@@ -235,11 +454,12 @@ 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 $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))" 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) {
@@ -256,6 +476,7 @@ try {
Write-Host "--- End ---" Write-Host "--- End ---"
} }
} }
Write-PlatformOutput $__v8.Output
exit $exitCode exit $exitCode
+278 -21
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-load-dt v1.9 — 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,6 +36,163 @@ 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): def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8).""" """Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p) parent = os.path.dirname(p)
@@ -86,6 +243,85 @@ IBCMD_NOUSER_HINT = (
) )
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): def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively. """Run an ibcmd command non-interactively.
@@ -96,7 +332,10 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
if warn_no_user and os.name == "nt" and not has_username: if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT) sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush() sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace") 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): def describe_exit(code):
@@ -148,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:
@@ -180,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 {_redact(' '.join(arguments), args.Password, args.UserName)}") arguments.extend(extra_args)
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_ibcmd([v8path] + arguments, bool(args.UserName)) result = run_ibcmd([v8path] + arguments, bool(args.UserName))
if result.returncode == 0: if result.returncode == 0:
print(f"Information base restored successfully from: {args.InputFile}") print(f"Information base restored successfully from: {args.InputFile}")
else: else:
print(f"Error restoring information base (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr) print(f"Error restoring information base (code: {result.returncode}){describe_exit(result.returncode)}", 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 ---
@@ -201,33 +460,30 @@ 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 {_redact(' '.join(arguments), args.Password, args.UserName)}") 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 ---
@@ -247,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:
+5 -3
View File
@@ -38,7 +38,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" <параметры> python "${CLAUDE_SKILL_DIR}/scripts/db-load-git.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -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`
@@ -70,8 +72,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" <
```powershell ```powershell
# Все незафиксированные изменения # Все незафиксированные изменения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source All -UpdateDB python "${CLAUDE_SKILL_DIR}/scripts/db-load-git.py" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source All -UpdateDB
# Из диапазона коммитов # Из диапазона коммитов
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source Commit -CommitRange "HEAD~3..HEAD" python "${CLAUDE_SKILL_DIR}/scripts/db-load-git.py" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source Commit -CommitRange "HEAD~3..HEAD"
``` ```
@@ -1,4 +1,4 @@
# db-load-git v1.15 — 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 не исполняется. # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
@@ -48,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
@@ -102,7 +108,13 @@ 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
@@ -115,6 +127,128 @@ function Protect-Secrets {
return $Text 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 { function Get-ExitAnnotation {
# Annotate an abnormal process exit code so a crash isn't reported as a bare number. # 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 # A batch DESIGNER that crashes (e.g. missing license) may leave the infobase locked or
@@ -191,32 +325,75 @@ 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 Invoke-IbcmdProcess { function ConvertFrom-PlatformBytes {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt # ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's # OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process. # one of them outright mangles Cyrillic.
param([string]$Exe, [string[]]$IbArgs) 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 = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe $psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' ' $psi.Arguments = if ($PreQuoted) {
$ProcArgs -join ' '
} else {
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
}
$psi.UseShellExecute = $false $psi.UseShellExecute = $false
$psi.CreateNoWindow = $true $psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true $psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $true $psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true $psi.RedirectStandardError = $true
try {
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
} catch {}
$p = [System.Diagnostics.Process]::Start($psi) $p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close() $p.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd() # stderr is drained in parallel: reading the streams one after another deadlocks
$err = $p.StandardError.ReadToEnd() # 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() $p.WaitForExit()
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
if ($err) { $out += $err } if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode } 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") {
@@ -230,6 +407,10 @@ function Invoke-IbcmdProcess {
} }
} }
# --- 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
@@ -396,24 +577,26 @@ 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 $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))" $arguments += $extraArgs
$__ib = Invoke-IbcmdProcess $V8Path $arguments Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__ib = Invoke-PlatformProcess $V8Path $arguments
$output = $__ib.Output $output = $__ib.Output
$exitCode = $__ib.ExitCode $exitCode = $__ib.ExitCode
if ($exitCode -ne 0) { if ($exitCode -ne 0) {
Write-Host "Error loading changes (code: $exitCode)$(Get-ExitAnnotation $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 $(Protect-Secrets ($applyArgs -join ' ') @($Password, $UserName))" $applyArgs += $extraArgs
$__ib = Invoke-IbcmdProcess $V8Path $applyArgs Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $applyArgs $engine) -join ' ') @($Password, $UserName))"
$__ib = Invoke-PlatformProcess $V8Path $applyArgs
$applyOut = $__ib.Output $applyOut = $__ib.Output
$exitCode = $__ib.ExitCode $exitCode = $__ib.ExitCode
if ($exitCode -eq 0) { if ($exitCode -eq 0) {
@@ -421,7 +604,7 @@ try {
} else { } else {
Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $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
} }
@@ -466,14 +649,15 @@ 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 $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))" 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 ""
@@ -491,6 +675,7 @@ try {
Write-Host "--- End ---" Write-Host "--- End ---"
} }
} }
Write-PlatformOutput $__v8.Output
exit $exitCode exit $exitCode
+282 -29
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-load-git v1.15 — 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,6 +36,163 @@ 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): def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8).""" """Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p) parent = os.path.dirname(p)
@@ -86,6 +243,85 @@ IBCMD_NOUSER_HINT = (
) )
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): def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively. """Run an ibcmd command non-interactively.
@@ -96,7 +332,10 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
if warn_no_user and os.name == "nt" and not has_username: if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT) sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush() sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace") 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):
@@ -185,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
@@ -204,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)
@@ -340,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 {_redact(' '.join(arguments), args.Password, args.UserName)}") arguments.extend(extra_args)
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_ibcmd([v8path] + arguments, bool(args.UserName)) result = run_ibcmd([v8path] + arguments, bool(args.UserName))
if result.returncode != 0: if result.returncode != 0:
print(f"Error loading changes (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr) print(f"Error loading changes (code: {result.returncode}){describe_exit(result.returncode)}", 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"]
@@ -360,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 {_redact(' '.join(apply_args), args.Password, args.UserName)}") apply_args.extend(extra_args)
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)) 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}){describe_exit(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) ---
@@ -382,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")
@@ -409,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 {_redact(' '.join(arguments), args.Password, args.UserName)}") 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 ---
@@ -442,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:
+7 -5
View File
@@ -38,7 +38,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" <параметры> python "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -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`
@@ -88,14 +90,14 @@ Documents/Заказ/Forms/ФормаДокумента.xml
```powershell ```powershell
# Полная загрузка # Полная загрузка
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full python "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.py" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full
# Частичная загрузка конкретных файлов # Частичная загрузка конкретных файлов
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl" python "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl"
# Загрузка расширения # Загрузка расширения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение" python "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
# Загрузка + обновление БД в одном запуске # Загрузка + обновление БД в одном запуске
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full -UpdateDB python "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full -UpdateDB
``` ```
@@ -1,4 +1,4 @@
# db-load-xml v1.16 — 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 не исполняется. # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
@@ -48,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
@@ -102,7 +108,13 @@ 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
@@ -132,6 +144,165 @@ function Get-ExitAnnotation {
return "" 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
@@ -176,35 +347,82 @@ if (-not (Test-Path $V8Path)) {
} }
# --- Detect engine (ibcmd vs 1cv8) by exe name --- # --- Detect engine (ibcmd vs 1cv8) by exe name ---
function Invoke-IbcmdProcess { function ConvertFrom-PlatformBytes {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt # ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's # OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process. # one of them outright mangles Cyrillic.
param([string]$Exe, [string[]]$IbArgs) 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 = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe $psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' ' $psi.Arguments = if ($PreQuoted) {
$ProcArgs -join ' '
} else {
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
}
$psi.UseShellExecute = $false $psi.UseShellExecute = $false
$psi.CreateNoWindow = $true $psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true $psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $true $psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true $psi.RedirectStandardError = $true
try {
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
} catch {}
$p = [System.Diagnostics.Process]::Start($psi) $p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close() $p.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd() # stderr is drained in parallel: reading the streams one after another deadlocks
$err = $p.StandardError.ReadToEnd() # 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() $p.WaitForExit()
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
if ($err) { $out += $err } if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode } 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) {
@@ -268,25 +486,27 @@ 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 $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))" $arguments += $extraArgs
$__ib = Invoke-IbcmdProcess $V8Path $arguments Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__ib = Invoke-PlatformProcess $V8Path $arguments
$output = $__ib.Output $output = $__ib.Output
$exitCode = $__ib.ExitCode $exitCode = $__ib.ExitCode
if ($exitCode -ne 0) { if ($exitCode -ne 0) {
Write-Host "Error loading configuration from files (code: $exitCode)$(Get-ExitAnnotation $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 $(Protect-Secrets ($applyArgs -join ' ') @($Password, $UserName))" $applyArgs += $extraArgs
$__ib = Invoke-IbcmdProcess $V8Path $applyArgs Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $applyArgs $engine) -join ' ') @($Password, $UserName))"
$__ib = Invoke-PlatformProcess $V8Path $applyArgs
$applyOut = $__ib.Output $applyOut = $__ib.Output
$exitCode = $__ib.ExitCode $exitCode = $__ib.ExitCode
if ($exitCode -eq 0) { if ($exitCode -eq 0) {
@@ -294,7 +514,7 @@ try {
} else { } else {
Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $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
} }
@@ -373,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 $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))" 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
@@ -424,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"
+283 -29
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-load-xml v1.16 — 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,6 +36,163 @@ 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): def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8).""" """Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p) parent = os.path.dirname(p)
@@ -86,6 +243,85 @@ IBCMD_NOUSER_HINT = (
) )
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): def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively. """Run an ibcmd command non-interactively.
@@ -96,7 +332,10 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
if warn_no_user and os.name == "nt" and not has_username: if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT) sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush() sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace") 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): def describe_exit(code):
@@ -168,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:
@@ -232,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 {_redact(' '.join(arguments), args.Password, args.UserName)}") arguments.extend(extra_args)
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_ibcmd([v8path] + arguments, bool(args.UserName)) result = run_ibcmd([v8path] + arguments, bool(args.UserName))
if result.returncode != 0: if result.returncode != 0:
print(f"Error loading configuration from files (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr) print(f"Error loading configuration from files (code: {result.returncode}){describe_exit(result.returncode)}", 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"]
@@ -252,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 {_redact(' '.join(apply_args), args.Password, args.UserName)}") apply_args.extend(extra_args)
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)) 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}){describe_exit(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 ---
@@ -274,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...")
@@ -319,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")
@@ -327,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")
@@ -337,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 {_redact(' '.join(arguments), args.Password, args.UserName)}") 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 ---
@@ -392,6 +645,7 @@ def main():
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(
+6 -5
View File
@@ -36,7 +36,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" <параметры> python "${CLAUDE_SKILL_DIR}/scripts/db-run.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -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`
@@ -63,14 +64,14 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" <пар
```powershell ```powershell
# Простой запуск # Простой запуск
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" python "${CLAUDE_SKILL_DIR}/scripts/db-run.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin"
# Запуск с обработкой # Запуск с обработкой
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Execute "C:\epf\МояОбработка.epf" python "${CLAUDE_SKILL_DIR}/scripts/db-run.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Execute "C:\epf\МояОбработка.epf"
# Открыть по навигационной ссылке # Открыть по навигационной ссылке
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -URL "e1cib/data/Справочник.Номенклатура" python "${CLAUDE_SKILL_DIR}/scripts/db-run.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -URL "e1cib/data/Справочник.Номенклатура"
# Серверная база с параметром запуска # Серверная база с параметром запуска
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -CParam "ЗапуститьОбновление" python "${CLAUDE_SKILL_DIR}/scripts/db-run.py" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -CParam "ЗапуститьОбновление"
``` ```
+178 -3
View File
@@ -1,4 +1,4 @@
# db-run v1.4 — 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 не исполняется. # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
@@ -36,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"
@@ -73,7 +79,13 @@ 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
@@ -86,6 +98,151 @@ function Protect-Secrets {
return $Text 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
@@ -129,6 +286,19 @@ if (-not (Test-Path $V8Path)) {
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
@@ -172,9 +342,14 @@ if ($URL) {
$argString += " /DisableStartupDialogs" $argString += " /DisableStartupDialogs"
# The display string is built from the same tokens with secret-prone values redacted.
$displayString = $argString
foreach ($tok in $extraArgs) { $argString += (Format-ArgToken $tok) }
foreach ($tok in (Format-ArgsForDisplay $extraArgs $engine)) { $displayString += (Format-ArgToken $tok) }
# --- Execute (background) --- # --- Execute (background) ---
# Redact the password/user before printing the command line — never leak secrets. # Redact the password/user before printing the command line — never leak secrets.
$displayArg = Protect-Secrets $argString @($Password, $UserName) $displayArg = Protect-Secrets $displayString @($Password, $UserName)
Write-Host "Running: 1cv8.exe $displayArg" Write-Host "Running: 1cv8.exe $displayArg"
$proc = Start-Process -FilePath $V8Path -ArgumentList $argString -PassThru $proc = Start-Process -FilePath $V8Path -ArgumentList $argString -PassThru
+203 -3
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-run v1.4 — 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
@@ -33,6 +33,181 @@ 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): def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8).""" """Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p) parent = os.path.dirname(p)
@@ -100,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)
@@ -140,10 +339,11 @@ def main():
arguments.extend(["/URL", args.URL]) arguments.extend(["/URL", args.URL])
arguments.append("/DisableStartupDialogs") arguments.append("/DisableStartupDialogs")
arguments.extend(extra_args)
# --- Execute (background) --- # --- Execute (background) ---
# Redact the password/user before printing the command line — never leak secrets. # Redact the password/user before printing the command line — never leak secrets.
print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}") print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
proc = subprocess.Popen([v8path] + arguments) proc = subprocess.Popen([v8path] + arguments)
# --- Bounded early-exit check --- # --- Bounded early-exit check ---
+6 -4
View File
@@ -35,7 +35,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" <параметры> python "${CLAUDE_SKILL_DIR}/scripts/db-update.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -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`
@@ -76,11 +78,11 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" <п
```powershell ```powershell
# Обычное обновление (файловая база) # Обычное обновление (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" python "${CLAUDE_SKILL_DIR}/scripts/db-update.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin"
# Динамическое обновление (серверная база) # Динамическое обновление (серверная база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -Dynamic "+" python "${CLAUDE_SKILL_DIR}/scripts/db-update.py" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -Dynamic "+"
# Обновление расширения # Обновление расширения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Extension "МоёРасширение" python "${CLAUDE_SKILL_DIR}/scripts/db-update.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Extension "МоёРасширение"
``` ```
+239 -20
View File
@@ -1,4 +1,4 @@
# db-update v1.10 — 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 не исполняется. # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
@@ -42,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"
@@ -83,7 +89,13 @@ 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
@@ -113,6 +125,163 @@ function Get-ExitAnnotation {
return "" 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
@@ -157,35 +326,82 @@ if (-not (Test-Path $V8Path)) {
} }
# --- Detect engine (ibcmd vs 1cv8) by exe name --- # --- Detect engine (ibcmd vs 1cv8) by exe name ---
function Invoke-IbcmdProcess { function ConvertFrom-PlatformBytes {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt # ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's # OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process. # one of them outright mangles Cyrillic.
param([string]$Exe, [string[]]$IbArgs) 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 = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe $psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' ' $psi.Arguments = if ($PreQuoted) {
$ProcArgs -join ' '
} else {
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
}
$psi.UseShellExecute = $false $psi.UseShellExecute = $false
$psi.CreateNoWindow = $true $psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true $psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $true $psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true $psi.RedirectStandardError = $true
try {
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
} catch {}
$p = [System.Diagnostics.Process]::Start($psi) $p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close() $p.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd() # stderr is drained in parallel: reading the streams one after another deadlocks
$err = $p.StandardError.ReadToEnd() # 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() $p.WaitForExit()
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
if ($err) { $out += $err } if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode } 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) {
@@ -215,8 +431,9 @@ 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 $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))" $arguments += $extraArgs
$__ib = Invoke-IbcmdProcess $V8Path $arguments Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__ib = Invoke-PlatformProcess $V8Path $arguments
$output = $__ib.Output $output = $__ib.Output
$exitCode = $__ib.ExitCode $exitCode = $__ib.ExitCode
if ($exitCode -eq 0) { if ($exitCode -eq 0) {
@@ -224,7 +441,7 @@ try {
} else { } else {
Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $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
} }
@@ -265,11 +482,12 @@ 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 $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))" 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) {
@@ -286,6 +504,7 @@ try {
Write-Host "--- End ---" Write-Host "--- End ---"
} }
} }
Write-PlatformOutput $__v8.Output
exit $exitCode exit $exitCode
+276 -20
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-update v1.10 — 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,6 +36,163 @@ 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): def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8).""" """Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p) parent = os.path.dirname(p)
@@ -86,6 +243,85 @@ IBCMD_NOUSER_HINT = (
) )
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): def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively. """Run an ibcmd command non-interactively.
@@ -96,7 +332,10 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
if warn_no_user and os.name == "nt" and not has_username: if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT) sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush() sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace") 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): def describe_exit(code):
@@ -150,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:
@@ -184,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 {_redact(' '.join(arguments), args.Password, args.UserName)}") arguments.extend(extra_args)
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_ibcmd([v8path] + arguments, bool(args.UserName)) result = run_ibcmd([v8path] + arguments, bool(args.UserName))
if result.returncode == 0: if result.returncode == 0:
print("Database configuration updated successfully") print("Database configuration updated successfully")
else: else:
print(f"Error updating database configuration (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr) print(f"Error updating database configuration (code: {result.returncode}){describe_exit(result.returncode)}", 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 ---
@@ -205,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")
@@ -226,22 +484,19 @@ 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 {_redact(' '.join(arguments), args.Password, args.UserName)}") 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 ---
@@ -261,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:
+5 -3
View File
@@ -40,7 +40,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" <параметры> python "${CLAUDE_SKILL_DIR}/scripts/epf-build.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -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` через запятую, в форме `--ключ=значение` |
> `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных > `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных
@@ -62,8 +64,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" <п
```powershell ```powershell
# Сборка обработки (файловая база) # Сборка обработки (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf" python "${CLAUDE_SKILL_DIR}/scripts/epf-build.py" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf"
# Серверная база # Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf" python "${CLAUDE_SKILL_DIR}/scripts/epf-build.py" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf"
``` ```
+255 -22
View File
@@ -1,4 +1,4 @@
# epf-build v1.9 — 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 не исполняется. # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
@@ -33,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"
@@ -64,7 +70,13 @@ 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
@@ -77,6 +89,165 @@ function Protect-Secrets {
return $Text 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
@@ -121,32 +292,75 @@ if (-not (Test-Path $V8Path)) {
} }
# --- Detect engine (ibcmd vs 1cv8) by exe name --- # --- Detect engine (ibcmd vs 1cv8) by exe name ---
function Invoke-IbcmdProcess { function ConvertFrom-PlatformBytes {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt # ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's # OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process. # one of them outright mangles Cyrillic.
param([string]$Exe, [string[]]$IbArgs) 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 = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe $psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' ' $psi.Arguments = if ($PreQuoted) {
$ProcArgs -join ' '
} else {
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
}
$psi.UseShellExecute = $false $psi.UseShellExecute = $false
$psi.CreateNoWindow = $true $psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true $psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $true $psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true $psi.RedirectStandardError = $true
try {
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
} catch {}
$p = [System.Diagnostics.Process]::Start($psi) $p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close() $p.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd() # stderr is drained in parallel: reading the streams one after another deadlocks
$err = $p.StandardError.ReadToEnd() # 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() $p.WaitForExit()
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
if ($err) { $out += $err } if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode } 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 { function Test-OutputNonEmpty {
# Postcondition: the platform must have produced a non-empty output file. # Postcondition: the platform must have produced a non-empty output file.
@@ -156,6 +370,10 @@ function Test-OutputNonEmpty {
} }
$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
@@ -168,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
@@ -202,8 +432,9 @@ 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 $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))" $arguments += $extraArgs
$__ib = Invoke-IbcmdProcess $V8Path $arguments Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__ib = Invoke-PlatformProcess $V8Path $arguments
$output = $__ib.Output $output = $__ib.Output
$exitCode = $__ib.ExitCode $exitCode = $__ib.ExitCode
$outMissing = ($exitCode -eq 0) -and -not (Test-OutputNonEmpty $OutputFile) $outMissing = ($exitCode -eq 0) -and -not (Test-OutputNonEmpty $OutputFile)
@@ -215,7 +446,7 @@ try {
} 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
} }
@@ -238,11 +469,12 @@ 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 $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))" 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. # Postcondition: exit 0 without a non-empty output file is a false success.
@@ -264,6 +496,7 @@ try {
Write-Host "--- End ---" Write-Host "--- End ---"
} }
} }
Write-PlatformOutput $__v8.Output
exit $exitCode exit $exitCode
+288 -24
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# epf-build v1.9 — 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,6 +36,163 @@ 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): def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8).""" """Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p) parent = os.path.dirname(p)
@@ -86,6 +243,85 @@ IBCMD_NOUSER_HINT = (
) )
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): def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively. """Run an ibcmd command non-interactively.
@@ -96,7 +332,10 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
if warn_no_user and os.name == "nt" and not has_username: if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT) sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush() sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace") 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): def output_nonempty(path):
@@ -129,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)
@@ -145,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)
@@ -181,7 +450,8 @@ 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 {_redact(' '.join(arguments), args.Password, args.UserName)}") arguments.extend(extra_args)
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_ibcmd([v8path] + arguments, warn_no_user=False) result = run_ibcmd([v8path] + arguments, warn_no_user=False)
exit_code = result.returncode exit_code = result.returncode
out_missing = exit_code == 0 and not output_nonempty(args.OutputFile) out_missing = exit_code == 0 and not output_nonempty(args.OutputFile)
@@ -193,39 +463,32 @@ def main():
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — build produced no output", file=sys.stderr) print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — build produced no output", file=sys.stderr)
else: else:
print(f"Error building external data processor/report (code: {exit_code})", file=sys.stderr) print(f"Error building external data processor/report (code: {exit_code})", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
sys.exit(exit_code) sys.exit(exit_code)
# --- 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 {_redact(' '.join(arguments), args.Password, args.UserName)}") 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 ---
@@ -251,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.3 — 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,34 +1403,89 @@ $propsXml </Properties>$childObjLine
} }
# --- 5a. Stub via ibcmd (one call: create [--import --apply]) --- # --- 5a. Stub via ibcmd (one call: create [--import --apply]) ---
function Invoke-IbcmdProcess { function ConvertFrom-PlatformBytes {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt # ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's # OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process. # one of them outright mangles Cyrillic.
param([string]$Exe, [string[]]$IbArgs) 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 = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe $psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' ' $psi.Arguments = if ($PreQuoted) {
$ProcArgs -join ' '
} else {
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
}
$psi.UseShellExecute = $false $psi.UseShellExecute = $false
$psi.CreateNoWindow = $true $psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true $psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $true $psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true $psi.RedirectStandardError = $true
try {
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
} catch {}
$p = [System.Diagnostics.Process]::Start($psi) $p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close() $p.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd() # stderr is drained in parallel: reading the streams one after another deadlocks
$err = $p.StandardError.ReadToEnd() # 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() $p.WaitForExit()
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
if ($err) { $out += $err } if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode } 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)"
@@ -1288,12 +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"
$__ib = Invoke-IbcmdProcess $V8Path $ibArgs $ibArgs += $extraArgs
$__ib = Invoke-PlatformProcess $V8Path $ibArgs
$ibOut = $__ib.Output $ibOut = $__ib.Output
$ibRc = $__ib.ExitCode $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
} }
@@ -1305,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
} }
@@ -1318,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
} }
@@ -1329,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.3 — 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
@@ -20,6 +20,75 @@ IBCMD_NOUSER_HINT = (
) )
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): def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively. """Run an ibcmd command non-interactively.
@@ -30,7 +99,167 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
if warn_no_user and os.name == "nt" and not has_username: if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT) sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush() sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace") 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():
@@ -802,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)
@@ -1057,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}')
@@ -1065,6 +1308,7 @@ 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}')
ib_args.extend(extra_args)
result = run_ibcmd(ib_args, warn_no_user=False) 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:
@@ -1083,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)
@@ -1095,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:
@@ -1117,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)
+5 -3
View File
@@ -39,7 +39,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" <параметры> python "${CLAUDE_SKILL_DIR}/scripts/epf-dump.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -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 в пустой базе безвозвратно теряет ссылочные типы)
@@ -62,8 +64,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" <па
```powershell ```powershell
# Разборка обработки (файловая база) # Разборка обработки (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МояОбработка.epf" -OutputDir "src" python "${CLAUDE_SKILL_DIR}/scripts/epf-dump.py" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МояОбработка.epf" -OutputDir "src"
# Серверная база # Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МояОбработка.epf" -OutputDir "src" python "${CLAUDE_SKILL_DIR}/scripts/epf-dump.py" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МояОбработка.epf" -OutputDir "src"
``` ```
+241 -20
View File
@@ -1,4 +1,4 @@
# epf-dump v1.8 — 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 не исполняется. # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
@@ -36,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"
@@ -71,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
@@ -128,32 +299,75 @@ 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 Invoke-IbcmdProcess { function ConvertFrom-PlatformBytes {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt # ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's # OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process. # one of them outright mangles Cyrillic.
param([string]$Exe, [string[]]$IbArgs) 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 = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe $psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' ' $psi.Arguments = if ($PreQuoted) {
$ProcArgs -join ' '
} else {
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
}
$psi.UseShellExecute = $false $psi.UseShellExecute = $false
$psi.CreateNoWindow = $true $psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true $psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $true $psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true $psi.RedirectStandardError = $true
try {
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
} catch {}
$p = [System.Diagnostics.Process]::Start($psi) $p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close() $p.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd() # stderr is drained in parallel: reading the streams one after another deadlocks
$err = $p.StandardError.ReadToEnd() # 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() $p.WaitForExit()
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
if ($err) { $out += $err } if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode } 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 { function Test-DirNonEmpty {
# Postcondition: the platform must have written files into the output directory. # Postcondition: the platform must have written files into the output directory.
@@ -170,6 +384,10 @@ function Protect-Secrets {
} }
$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
@@ -203,8 +421,9 @@ 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 $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))" $arguments += $extraArgs
$__ib = Invoke-IbcmdProcess $V8Path $arguments Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__ib = Invoke-PlatformProcess $V8Path $arguments
$output = $__ib.Output $output = $__ib.Output
$exitCode = $__ib.ExitCode $exitCode = $__ib.ExitCode
$outMissing = ($exitCode -eq 0) -and -not (Test-DirNonEmpty $OutputDir) $outMissing = ($exitCode -eq 0) -and -not (Test-DirNonEmpty $OutputDir)
@@ -216,7 +435,7 @@ try {
} 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
} }
@@ -240,11 +459,12 @@ 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 $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))" 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. # Postcondition: exit 0 with an empty output directory is a false success.
@@ -266,6 +486,7 @@ try {
Write-Host "--- End ---" Write-Host "--- End ---"
} }
} }
Write-PlatformOutput $__v8.Output
exit $exitCode exit $exitCode
+278 -20
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# epf-dump v1.8 — 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,6 +36,163 @@ 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): def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8).""" """Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p) parent = os.path.dirname(p)
@@ -86,6 +243,85 @@ IBCMD_NOUSER_HINT = (
) )
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): def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively. """Run an ibcmd command non-interactively.
@@ -96,7 +332,10 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
if warn_no_user and os.name == "nt" and not has_username: if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT) sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush() sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace") 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): def dir_nonempty(path):
@@ -135,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)
@@ -178,7 +441,8 @@ 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 {_redact(' '.join(arguments), args.Password, args.UserName)}") arguments.extend(extra_args)
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_ibcmd([v8path] + arguments, warn_no_user=False) result = run_ibcmd([v8path] + arguments, warn_no_user=False)
exit_code = result.returncode exit_code = result.returncode
out_missing = exit_code == 0 and not dir_nonempty(args.OutputDir) out_missing = exit_code == 0 and not dir_nonempty(args.OutputDir)
@@ -190,40 +454,33 @@ def main():
print(f"Error: exit code 0 but no files under {args.OutputDir} — dump produced no output", file=sys.stderr) print(f"Error: exit code 0 but no files under {args.OutputDir} — dump produced no output", file=sys.stderr)
else: else:
print(f"Error dumping external data processor/report (code: {exit_code})", file=sys.stderr) print(f"Error dumping external data processor/report (code: {exit_code})", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
sys.exit(exit_code) sys.exit(exit_code)
# --- 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 {_redact(' '.join(arguments), args.Password, args.UserName)}") 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 ---
@@ -249,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 -1
View File
@@ -30,7 +30,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] python "${CLAUDE_SKILL_DIR}/scripts/init.py" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"]
``` ```
## Дальнейшие шаги ## Дальнейшие шаги
+2 -2
View File
@@ -24,7 +24,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-validate.ps1" -ObjectPath "src/МояОбработка" python "${CLAUDE_SKILL_DIR}/scripts/epf-validate.py" -ObjectPath "src/МояОбработка"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-validate.ps1" -ObjectPath "src/МояОбработка/МояОбработка.xml" python "${CLAUDE_SKILL_DIR}/scripts/epf-validate.py" -ObjectPath "src/МояОбработка/МояОбработка.xml"
``` ```
@@ -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 = []
+5 -3
View File
@@ -42,7 +42,7 @@ allowed-tools:
Используй общий скрипт из epf-build: Используй общий скрипт из epf-build:
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.ps1" <параметры> python "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -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` через запятую, в форме `--ключ=значение` |
> `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных > `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных
@@ -64,8 +66,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-bu
```powershell ```powershell
# Сборка отчёта (файловая база) # Сборка отчёта (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.ps1" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf" python "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.py" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf"
# Серверная база # Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf" python "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.py" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf"
``` ```
+5 -3
View File
@@ -41,7 +41,7 @@ allowed-tools:
Используй общий скрипт из epf-dump: Используй общий скрипт из epf-dump:
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.ps1" <параметры> python "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -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 в пустой базе безвозвратно теряет ссылочные типы)
@@ -64,8 +66,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dum
```powershell ```powershell
# Разборка отчёта (файловая база) # Разборка отчёта (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.ps1" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МойОтчёт.erf" -OutputDir "src" python "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.py" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МойОтчёт.erf" -OutputDir "src"
# Серверная база # Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МойОтчёт.erf" -OutputDir "src" python "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.py" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МойОтчёт.erf" -OutputDir "src"
``` ```
+1 -1
View File
@@ -31,7 +31,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-WithSKD] python "${CLAUDE_SKILL_DIR}/scripts/init.py" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-WithSKD]
``` ```
## Дальнейшие шаги ## Дальнейшие шаги
+2 -2
View File
@@ -26,7 +26,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-validate/scripts/epf-validate.ps1" -ObjectPath "src/МойОтчёт" python "${CLAUDE_SKILL_DIR}/../epf-validate/scripts/epf-validate.py" -ObjectPath "src/МойОтчёт"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-validate/scripts/epf-validate.ps1" -ObjectPath "src/МойОтчёт/МойОтчёт.xml" python "${CLAUDE_SKILL_DIR}/../epf-validate/scripts/epf-validate.py" -ObjectPath "src/МойОтчёт/МойОтчёт.xml"
``` ```
+1 -1
View File
@@ -32,7 +32,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-add.ps1" -ObjectPath "<ObjectPath>" -FormName "<FormName>" [-Purpose "<Purpose>"] [-Synonym "<Synonym>"] [-SetDefault] python "${CLAUDE_SKILL_DIR}/scripts/form-add.py" -ObjectPath "<ObjectPath>" -FormName "<FormName>" [-Purpose "<Purpose>"] [-Synonym "<Synonym>"] [-SetDefault]
``` ```
## Purpose — назначение формы ## Purpose — назначение формы
+15 -4
View File
@@ -1,4 +1,4 @@
# form-add v1.10 — 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)]
@@ -156,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
@@ -484,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
@@ -538,6 +544,7 @@ if ($insertBefore) {
} }
} }
} }
}
# --- SetDefault --- # --- SetDefault ---
@@ -603,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"
} }
+8 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# form-add v1.10 — 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
@@ -592,7 +592,10 @@ 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)
already_registered = child_objects.find(f"md:Form[.='{form_name}']", NSMAP) is not None
if not already_registered:
form_elem = etree.Element(f"{{{ns}}}Form") form_elem = etree.Element(f"{{{ns}}}Form")
form_elem.text = form_name form_elem.text = form_name
@@ -677,6 +680,9 @@ 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()
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") 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}")
+4 -4
View File
@@ -29,10 +29,10 @@ allowed-tools:
```powershell ```powershell
# Режим JSON DSL # Режим JSON DSL
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-compile.ps1" -JsonPath "<json>" -OutputPath "<Form.xml>" python "${CLAUDE_SKILL_DIR}/scripts/form-compile.py" -JsonPath "<json>" -OutputPath "<Form.xml>"
# Режим from-object (объект и purpose выводятся из OutputPath; Document и Catalog) # Режим from-object (объект и purpose выводятся из OutputPath; Document и Catalog)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-compile.ps1" -FromObject -OutputPath "<.../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml>" python "${CLAUDE_SKILL_DIR}/scripts/form-compile.py" -FromObject -OutputPath "<.../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml>"
``` ```
## JSON DSL — справка ## JSON DSL — справка
@@ -550,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.175 — 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
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# form-compile v1.175 — 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
+1 -1
View File
@@ -31,7 +31,7 @@ allowed-tools:
| `OutputPath` | Путь к выходному JSON. Если не задан — JSON в stdout | | `OutputPath` | Путь к выходному JSON. Если не задан — JSON в stdout |
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-decompile.ps1" -FormPath "<Form.xml>" -OutputPath "<out.json>" python "${CLAUDE_SKILL_DIR}/scripts/form-decompile.py" -FormPath "<Form.xml>" -OutputPath "<out.json>"
``` ```
## Что получаешь ## Что получаешь
+1 -1
View File
@@ -29,7 +29,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-edit.ps1" -FormPath "<путь>" -JsonPath "<путь>" python "${CLAUDE_SKILL_DIR}/scripts/form-edit.py" -FormPath "<путь>" -JsonPath "<путь>"
``` ```
## JSON формат ## JSON формат
@@ -1,4 +1,4 @@
# form-edit v1.5 — 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)]
@@ -270,8 +270,11 @@ function X {
} }
function Esc-Xml { function Esc-Xml {
# Экранирование ТЕКСТА элемента: только & < > . Кавычки в тексте платформа НЕ экранирует —
# пишет литерально (проверено: 92142 сырых кавычки на корпус, ни одной &quot;). &quot; платформа
# принимает, но при выгрузке нормализует обратно в кавычку → лишний шум в роундтрипе.
param([string]$s) param([string]$s)
return $s.Replace('&','&amp;').Replace('<','&lt;').Replace('>','&gt;').Replace('"','&quot;') return $s.Replace('&','&amp;').Replace('<','&lt;').Replace('>','&gt;')
} }
function Emit-MLText { function Emit-MLText {
@@ -1,4 +1,4 @@
# form-edit v1.5 — 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
@@ -226,7 +226,9 @@ def local_name(node):
# ── helpers ────────────────────────────────────────────────── # ── helpers ──────────────────────────────────────────────────
def esc_xml(s): def esc_xml(s):
return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;') """Экранирование ТЕКСТА элемента: только & < > . Кавычки платформа в тексте не экранирует
(92142 сырых кавычки на корпус, ни одной &quot;); &quot; она принимает, но нормализует обратно."""
return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
# ── 1. Load Form.xml ──────────────────────────────────────── # ── 1. Load Form.xml ────────────────────────────────────────
+1 -1
View File
@@ -15,7 +15,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-info.ps1" -FormPath "<путь к Form.xml>" python "${CLAUDE_SKILL_DIR}/scripts/form-info.py" -FormPath "<путь к Form.xml>"
``` ```
## Параметры ## Параметры
+1 -1
View File
@@ -31,7 +31,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/remove-form.ps1" -ObjectName "<ObjectName>" -FormName "<FormName>" [-SrcDir "<SrcDir>"] python "${CLAUDE_SKILL_DIR}/scripts/remove-form.py" -ObjectName "<ObjectName>" -FormName "<FormName>" [-SrcDir "<SrcDir>"]
``` ```
## Что удаляется ## Что удаляется
+2 -2
View File
@@ -23,7 +23,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-validate.ps1" -FormPath "Catalogs/Номенклатура/Forms/ФормаЭлемента" python "${CLAUDE_SKILL_DIR}/scripts/form-validate.py" -FormPath "Catalogs/Номенклатура/Forms/ФормаЭлемента"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-validate.ps1" -FormPath "src/МояОбработка/Forms/Форма/Ext/Form.xml" python "${CLAUDE_SKILL_DIR}/scripts/form-validate.py" -FormPath "src/МояОбработка/Forms/Форма/Ext/Form.xml"
``` ```
@@ -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 -1
View File
@@ -30,7 +30,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/add-help.ps1" -ObjectName "<ObjectName>" [-Lang "<Lang>"] [-SrcDir "<SrcDir>"] python "${CLAUDE_SKILL_DIR}/scripts/add-help.py" -ObjectName "<ObjectName>" [-Lang "<Lang>"] [-SrcDir "<SrcDir>"]
``` ```
## Что делает скрипт ## Что делает скрипт
+2 -2
View File
@@ -29,13 +29,13 @@ allowed-tools:
### Inline mode ### Inline mode
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/interface-edit.ps1" -CIPath '<path>' -Operation hide -Value '<cmd>' python "${CLAUDE_SKILL_DIR}/scripts/interface-edit.py" -CIPath '<path>' -Operation hide -Value '<cmd>'
``` ```
### JSON mode ### JSON mode
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/interface-edit.ps1" -CIPath '<path>' -DefinitionFile '<json>' python "${CLAUDE_SKILL_DIR}/scripts/interface-edit.py" -CIPath '<path>' -DefinitionFile '<json>'
``` ```
## Операции ## Операции
@@ -1,4 +1,4 @@
# interface-edit v1.8 — 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,
@@ -164,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.8 — 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
+2 -2
View File
@@ -24,6 +24,6 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/interface-validate.ps1" -CIPath "Subsystems/Продажи" python "${CLAUDE_SKILL_DIR}/scripts/interface-validate.py" -CIPath "Subsystems/Продажи"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/interface-validate.ps1" -CIPath "Subsystems/Продажи/Ext/CommandInterface.xml" python "${CLAUDE_SKILL_DIR}/scripts/interface-validate.py" -CIPath "Subsystems/Продажи/Ext/CommandInterface.xml"
``` ```
+1 -1
View File
@@ -24,7 +24,7 @@ allowed-tools:
3. Изменить созданный объект — `/meta-edit`; проверить — `/meta-validate`. 3. Изменить созданный объект — `/meta-edit`; проверить — `/meta-validate`.
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-compile.ps1" -JsonPath "<json>" -OutputDir "<ConfigDir>" python "${CLAUDE_SKILL_DIR}/scripts/meta-compile.py" -JsonPath "<json>" -OutputDir "<ConfigDir>"
``` ```
| Параметр | Описание | | Параметр | Описание |
@@ -125,6 +125,7 @@ shorthand — вместо строки задаётся объект:
| `use` | `ForItem` | `ForItem` / `ForFolder` / `ForFolderAndItem` (только Catalog / ChartOfCharacteristicTypes) | | `use` | `ForItem` | `ForItem` / `ForFolder` / `ForFolderAndItem` (только Catalog / ChartOfCharacteristicTypes) |
| `attributes` | `[]` | колонки (shorthand или объектная форма реквизита) | | `attributes` | `[]` | колонки (shorthand или объектная форма реквизита) |
| `lineNumber` | — | кастомизация стандартного реквизита НомерСтроки (см. ниже) | | `lineNumber` | — | кастомизация стандартного реквизита НомерСтроки (см. ниже) |
| `lineNumberLength` | по режиму совместимости | `5``9` — разрядность номера строки: `5` → до 99 999 строк, `9` → до 999 999 999. Требует формата 2.20 (платформа 8.3.27) |
### `lineNumber` — стандартный реквизит НомерСтроки ### `lineNumber` — стандартный реквизит НомерСтроки
@@ -15,7 +15,7 @@
| `limitLevelCount` | `false` | bool (ограничивать кол-во уровней) | | `limitLevelCount` | `false` | bool (ограничивать кол-во уровней) |
| `levelCount` | `2` | число уровней (при `limitLevelCount`) | | `levelCount` | `2` | число уровней (при `limitLevelCount`) |
| `foldersOnTop` | `true` | bool (группы сверху) | | `foldersOnTop` | `true` | bool (группы сверху) |
| `owners` | `[]` | массив ссылок-владельцев: `["CatalogRef.Контрагенты"]` | | `owners` | `[]` | массив владельцев: `["Catalog.Контрагенты"]` |
| `subordinationUse` | `ToItems` | `ToItems` / `ToFolders` / `ToFoldersAndItems` (кому подчинён) | | `subordinationUse` | `ToItems` | `ToItems` / `ToFolders` / `ToFoldersAndItems` (кому подчинён) |
| `codeLength` | `9` | длина кода (0 — без кода) | | `codeLength` | `9` | длина кода (0 — без кода) |
| `codeType` | `String` | `String` / `Number` | | `codeType` | `String` | `String` / `Number` |
+20 -7
View File
@@ -7,16 +7,21 @@
| Ключ | Умолчание | Значения | | Ключ | Умолчание | Значения |
|------|-----------|----------| |------|-----------|----------|
| `rootURL` | `= name` (в нижнем регистре) | корневой URL | | `rootURL` | `= name` (в нижнем регистре) | корневой URL |
| `reuseSessions` | `DontUse` | `DontUse` / `AutoUse` | | `reuseSessions` | `DontUse` | `DontUse` / `Use` / `AutoUse` |
| `sessionMaxAge` | `20` | время жизни сессии, сек | | `sessionMaxAge` | `20` | время жизни сессии, сек |
| `urlTemplates` | `{}` | шаблоны URL (см. ниже) | | `urlTemplates` | `{}` | шаблоны URL (см. ниже) |
`urlTemplates` — объект `{ "ИмяШаблона": def }`, где `def`: `urlTemplates` — объект `{ "ИмяШаблона": def }`, где `def`:
- строка — URL-путь без методов: `"/health"`; - строка — URL-путь без методов: `"/health"`;
- объект: `template` (путь с параметрами `{id}`, по умолчанию `/имяшаблона`), `methods``{ "ИмяМетода": "HTTPMethod" }`. - объект: `template` (путь с параметрами `{id}`, по умолчанию `/имяшаблона`), `synonym`, `comment`,
`methods``{ "ИмяМетода": def }`.
`methods` — значение либо строка (только HTTP-метод), либо объект: `httpMethod`, `handler`,
`synonym`, `comment`.
HTTP-методы: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS`, `CONNECT`, `TRACE`, `MERGE`. HTTP-методы: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS`, `CONNECT`, `TRACE`, `MERGE`.
Обработчик метода в модуле именуется `{ИмяШаблона}{ИмяМетода}`. Обработчик по умолчанию именуется `{ИмяШаблона}{ИмяМетода}`; в типовых конфигурациях он часто
произвольный — тогда задавайте `handler` явно.
```json ```json
{ "type": "HTTPService", "name": "API", "rootURL": "api", { "type": "HTTPService", "name": "API", "rootURL": "api",
@@ -31,21 +36,29 @@ HTTP-методы: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS`, `
| Ключ | Умолчание | Значения | | Ключ | Умолчание | Значения |
|------|-----------|----------| |------|-----------|----------|
| `namespace` | пусто | URI пространства имён WSDL | | `namespace` | пусто | URI пространства имён WSDL |
| `xdtoPackages` | пусто | XDTO-пакеты | | `xdtoPackages` | пусто | список пакетов (см. ниже) |
| `reuseSessions` | `DontUse` | `DontUse` / `AutoUse` | | `descriptorFileName` | `= name` + `.1cws` | имя файла дескриптора |
| `reuseSessions` | `DontUse` | `DontUse` / `Use` / `AutoUse` |
| `sessionMaxAge` | `20` | время жизни сессии, сек | | `sessionMaxAge` | `20` | время жизни сессии, сек |
| `operations` | `{}` | операции (см. ниже) | | `operations` | `{}` | операции (см. ниже) |
`xdtoPackages`**массив** значений: `"XDTOPackage.Имя"` — пакет конфигурации, любое другое
значение — URI внешнего пространства имён (например `"http://v8.1c.ru/8.3/data/ext"`).
`operations` — объект `{ "ИмяОперации": def }`, где `def`: `operations` — объект `{ "ИмяОперации": def }`, где `def`:
- строка — XDTO-тип возврата без параметров: `"xs:string"`; - строка — XDTO-тип возврата без параметров: `"xs:string"`;
- объект: `returnType` (по умолчанию `xs:string`), `nillable` (bool), `transactioned` (bool), - объект: `returnType` (по умолчанию `xs:string`), `nillable` (bool), `transactioned` (bool),
`handler` (имя процедуры, по умолчанию = имя операции), `parameters`. `procedureName` (имя процедуры, по умолчанию = имя операции; синоним ключа — `handler`),
`dataLockControlMode` (по умолчанию `Managed`), `synonym`, `comment`, `parameters`.
`parameters` — объект `{ "ИмяПараметра": def }`, где `def`: `parameters` — объект `{ "ИмяПараметра": def }`, где `def`:
- строка — XDTO-тип (`direction` = `In`); - строка — XDTO-тип (`direction` = `In`);
- объект: `type` (по умолчанию `xs:string`), `nillable` (bool, по умолчанию `true`), `direction` (`In` / `Out` / `InOut`). - объект: `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`. XDTO-типы: `xs:string`, `xs:boolean`, `xs:int`, `xs:long`, `xs:decimal`, `xs:dateTime`, `xs:base64Binary`.
Тип из собственного пространства имён задаётся в нотации Кларка — `"{http://ваш.uri}ИмяТипа"`;
компилятор сам объявит локальный `xmlns` в теге, как это делает платформа.
```json ```json
{ "type": "WebService", "name": "DataExchange", "namespace": "http://www.1c.ru/DataExchange", { "type": "WebService", "name": "DataExchange", "namespace": "http://www.1c.ru/DataExchange",
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -38,7 +38,7 @@ allowed-tools:
| `OutputPath` | Путь к выходному JSON. Если не задан — JSON в stdout | | `OutputPath` | Путь к выходному JSON. Если не задан — JSON в stdout |
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-decompile.ps1" -ObjectPath "<Объект.xml>" -OutputPath "<out.json>" python "${CLAUDE_SKILL_DIR}/scripts/meta-decompile.py" -ObjectPath "<Объект.xml>" -OutputPath "<out.json>"
``` ```
Неподдерживаемый тип объекта или не-`MetaDataObject` root → ненулевой код выхода и сообщение в stderr. Неподдерживаемый тип объекта или не-`MetaDataObject` root → ненулевой код выхода и сообщение в stderr.
@@ -1,4 +1,4 @@
# meta-decompile v0.54 — XML объекта метаданных 1С → JSON-черновик формата meta-compile # meta-decompile v0.63 — XML объекта метаданных 1С → JSON-черновик формата meta-compile
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# #
# Поддержаны: Catalog, ExchangePlan, ChartOfCharacteristicTypes, ChartOfAccounts, ChartOfCalculationTypes, Document, # Поддержаны: Catalog, ExchangePlan, ChartOfCharacteristicTypes, ChartOfAccounts, ChartOfCalculationTypes, Document,
@@ -92,7 +92,7 @@ foreach ($c in $rootEl.ChildNodes) { if ($c.NodeType -eq 'Element') { $objNode =
if (-not $objNode) { [Console]::Error.WriteLine("meta-decompile: пустой MetaDataObject"); exit 3 } if (-not $objNode) { [Console]::Error.WriteLine("meta-decompile: пустой MetaDataObject"); exit 3 }
$objType = $objNode.LocalName $objType = $objNode.LocalName
if ($objType -notin @('Catalog', 'ExchangePlan', 'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'ChartOfCalculationTypes', 'Document', 'InformationRegister', 'AccumulationRegister', 'AccountingRegister', 'CalculationRegister', 'BusinessProcess', 'Task', 'Enum', 'Report', 'DataProcessor', 'Constant', 'DefinedType', 'FunctionalOption', 'DocumentJournal', 'Sequence', 'FilterCriterion', 'DocumentNumerator', 'SettingsStorage', 'CommonModule', 'EventSubscription', 'ScheduledJob', 'CommonForm', 'SessionParameter', 'CommonCommand', 'CommandGroup', 'CommonAttribute', 'FunctionalOptionsParameter', 'WSReference', 'CommonPicture', 'CommonTemplate')) { if ($objType -notin @('Catalog', 'ExchangePlan', 'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'ChartOfCalculationTypes', 'Document', 'InformationRegister', 'AccumulationRegister', 'AccountingRegister', 'CalculationRegister', 'BusinessProcess', 'Task', 'Enum', 'Report', 'DataProcessor', 'Constant', 'DefinedType', 'FunctionalOption', 'DocumentJournal', 'Sequence', 'FilterCriterion', 'DocumentNumerator', 'SettingsStorage', 'CommonModule', 'EventSubscription', 'ScheduledJob', 'CommonForm', 'SessionParameter', 'CommonCommand', 'CommandGroup', 'CommonAttribute', 'FunctionalOptionsParameter', 'WSReference', 'CommonPicture', 'CommonTemplate', 'HTTPService', 'WebService')) {
[Console]::Error.WriteLine("meta-decompile: тип '$objType' пока не поддержан (…, CommonPicture, CommonTemplate)"); exit 3 [Console]::Error.WriteLine("meta-decompile: тип '$objType' пока не поддержан (…, CommonPicture, CommonTemplate)"); exit 3
} }
@@ -219,6 +219,9 @@ function Get-TypeShorthand {
# string/dateTime/DesignTimeRef → строка (компилятор auto-детектит обратно). # string/dateTime/DesignTimeRef → строка (компилятор auto-детектит обратно).
function Convert-ChScalarNode { function Convert-ChScalarNode {
param($vN) param($vN)
# nil-элемент массива (<v8:Value xsi:nil="true"/>) → JSON null. Без этого он приезжал пустой
# строкой и компилятор эмитил xs:string вместо nil.
if ($vN.GetAttribute('nil', 'http://www.w3.org/2001/XMLSchema-instance') -eq 'true') { return $null }
$xt = $vN.GetAttribute('type', 'http://www.w3.org/2001/XMLSchema-instance') $xt = $vN.GetAttribute('type', 'http://www.w3.org/2001/XMLSchema-instance')
$txt = $vN.InnerText $txt = $vN.InnerText
if ($xt -match 'boolean$') { return ($txt -eq 'true') } if ($xt -match 'boolean$') { return ($txt -eq 'true') }
@@ -226,6 +229,9 @@ function Convert-ChScalarNode {
if ($txt -match '^-?\d+$') { return [int]$txt } if ($txt -match '^-?\d+$') { return [int]$txt }
return [double]::Parse($txt, [System.Globalization.CultureInfo]::InvariantCulture) return [double]::Parse($txt, [System.Globalization.CultureInfo]::InvariantCulture)
} }
# Пустой DesignTimeRef ≠ пустая строка: без маркера тип терялся, и компилятор эмитил xs:string.
# Та же конвенция, что у fillValue (см. ниже) — маркер emptyRef.
if ($xt -match 'DesignTimeRef$' -and $txt -eq '') { return [ordered]@{ emptyRef = $true } }
return $txt return $txt
} }
# app:value (тип прямо на узле) → значение ЛИБО массив (v8:FixedArray с детьми v8:Value). # app:value (тип прямо на узле) → значение ЛИБО массив (v8:FixedArray с детьми v8:Value).
@@ -323,6 +329,9 @@ function Attr-ToDsl {
$v = & $en 'MainFilter'; if ($v -eq 'true') { $extra['mainFilter'] = $true } $v = & $en 'MainFilter'; if ($v -eq 'true') { $extra['mainFilter'] = $true }
$v = & $en 'DenyIncompleteValues'; if ($v -eq 'true') { $extra['denyIncompleteValues'] = $true } $v = & $en 'DenyIncompleteValues'; if ($v -eq 'true') { $extra['denyIncompleteValues'] = $true }
$v = & $en 'UseInTotals'; if ($v -eq 'false') { $extra['useInTotals'] = $false } # дефолт true → захват при false $v = & $en 'UseInTotals'; if ($v -eq 'false') { $extra['useInTotals'] = $false } # дефолт true → захват при false
# Формат 2.20: режим приведения типов измерения РС. Дефолт TransformValues (его компилятор
# эмитит сам) → захватываем только отклонение.
$v = & $en 'TypeReductionMode'; if ($v -and $v -ne 'TransformValues') { $extra['typeReductionMode'] = $v }
$v = & $en 'BaseDimension'; if ($v -eq 'true') { $extra['baseDimension'] = $true } $v = & $en 'BaseDimension'; if ($v -eq 'true') { $extra['baseDimension'] = $true }
$v = & $en 'ScheduleLink'; if ($v) { $extra['scheduleLink'] = $v } # ссылка на измерение графика (пустой → пропуск) $v = & $en 'ScheduleLink'; if ($v) { $extra['scheduleLink'] = $v } # ссылка на измерение графика (пустой → пропуск)
$v = & $en 'Balance'; if ($v -eq 'true') { $extra['balance'] = $true } $v = & $en 'Balance'; if ($v -eq 'true') { $extra['balance'] = $true }
@@ -426,7 +435,9 @@ $cmt = P 'Comment'; if ($cmt) { $dsl['comment'] = $cmt }
# Свойства Catalog (omit-on-default). Порядок ключей — как удобно DSL. # Свойства Catalog (omit-on-default). Порядок ключей — как удобно DSL.
function Add-BoolProp { param([string]$key, [string]$tag, [bool]$default) $v = P $tag; if ($null -ne $v) { $b = ($v -eq 'true'); if ($b -ne $default) { $dsl[$key] = $b } } } function Add-BoolProp { param([string]$key, [string]$tag, [bool]$default) $v = P $tag; if ($null -ne $v) { $b = ($v -eq 'true'); if ($b -ne $default) { $dsl[$key] = $b } } }
function Add-EnumProp { param([string]$key, [string]$tag, [string]$default) $v = P $tag; if ($null -ne $v -and $v -ne '' -and $v -ne $default) { $dsl[$key] = $v } } # -cne: сравнение с дефолтом ВСЕГДА регистрочувствительное. PS -ne регистронезависим, и значение,
# отличающееся от дефолта только регистром, молча терялось (ловилось трижды: синонимы, RootURL).
function Add-EnumProp { param([string]$key, [string]$tag, [string]$default) $v = P $tag; if ($null -ne $v -and $v -ne '' -and $v -cne $default) { $dsl[$key] = $v } }
function Add-IntProp { param([string]$key, [string]$tag, [int]$default) $v = P $tag; if ($null -ne $v -and $v -ne '') { $iv = [int]$v; if ($iv -ne $default) { $dsl[$key] = $iv } } } function Add-IntProp { param([string]$key, [string]$tag, [int]$default) $v = P $tag; if ($null -ne $v -and $v -ne '') { $iv = [int]$v; if ($iv -ne $default) { $dsl[$key] = $iv } } }
Add-BoolProp 'hierarchical' 'Hierarchical' $false Add-BoolProp 'hierarchical' 'Hierarchical' $false
@@ -710,7 +721,7 @@ if ($objType -eq 'CommonForm') {
if ($upNode) { if ($upNode) {
$ups = @($upNode.SelectNodes('v8:Value', $nsm) | ForEach-Object { $_.InnerText }) $ups = @($upNode.SelectNodes('v8:Value', $nsm) | ForEach-Object { $_.InnerText })
$def2 = @('PlatformApplication', 'MobilePlatformApplication') $def2 = @('PlatformApplication', 'MobilePlatformApplication')
$same = ($ups.Count -eq $def2.Count); if ($same) { for ($k=0; $k -lt $ups.Count; $k++) { if ($ups[$k] -ne $def2[$k]) { $same=$false; break } } } $same = ($ups.Count -eq $def2.Count); if ($same) { for ($k=0; $k -lt $ups.Count; $k++) { if ($ups[$k] -cne $def2[$k]) { $same=$false; break } } }
if (-not $same -and $ups.Count -gt 0) { $dsl['usePurposes'] = [System.Collections.ArrayList]@($ups) } if (-not $same -and $ups.Count -gt 0) { $dsl['usePurposes'] = [System.Collections.ArrayList]@($ups) }
} }
$ep = Get-MLValue ($props.SelectSingleNode('md:ExtendedPresentation', $nsm)); if ($null -ne $ep) { $dsl['extendedPresentation'] = $ep } $ep = Get-MLValue ($props.SelectSingleNode('md:ExtendedPresentation', $nsm)); if ($null -ne $ep) { $dsl['extendedPresentation'] = $ep }
@@ -772,6 +783,43 @@ if ($objType -eq 'CommonCommand') {
Add-BoolProp 'modifiesData' 'ModifiesData' $false Add-BoolProp 'modifiesData' 'ModifiesData' $false
Add-EnumProp 'onMainServerUnavalableBehavior' 'OnMainServerUnavalableBehavior' 'Auto' Add-EnumProp 'onMainServerUnavalableBehavior' 'OnMainServerUnavalableBehavior' 'Auto'
} }
# XDTO-тип из элемента: если значение с префиксом (d6p1:Local) — разворачиваем префикс в URI и
# отдаём в нотации Кларка "{uri}Local"; префиксы платформы (dNpM) произвольны и переносу не подлежат.
function Get-XDTOTypeValue {
param($node)
if (-not $node) { return $null }
$txt = $node.InnerText
if ($txt -match '^([\w.-]+):(.+)$') {
$prefix = $Matches[1]; $local = $Matches[2]
$uri = $node.GetNamespaceOfPrefix($prefix)
# xs: и прочие стандартные оставляем как есть — компилятор их пишет дословно.
if ($uri -and $prefix -notin @('xs','xsi','v8','xr')) { return "{$uri}$local" }
}
return $txt
}
# WebService — пространство имён, состав XDTO-пакетов, дескриптор, операции с параметрами.
if ($objType -eq 'WebService') {
$ns = P 'Namespace'; if ($ns) { $dsl['namespace'] = $ns }
$pkgNodes = @($props.SelectNodes('md:XDTOPackages/xr:Item/xr:Value', $nsm))
if ($pkgNodes.Count -gt 0) {
$pkgs = [System.Collections.ArrayList]@()
foreach ($pn in $pkgNodes) { [void]$pkgs.Add($pn.InnerText) }
$dsl['xdtoPackages'] = $pkgs
}
$dfn = P 'DescriptorFileName'
if ($dfn -and $dfn -cne "$objName.1cws") { $dsl['descriptorFileName'] = $dfn }
Add-EnumProp 'reuseSessions' 'ReuseSessions' 'DontUse'
Add-IntProp 'sessionMaxAge' 'SessionMaxAge' 20
}
# HTTPService — корневой URL, повторное использование сеансов, время жизни сеанса.
# Шаблоны URL с методами разбираются в блоке ChildObjects.
if ($objType -eq 'HTTPService') {
# -cne: дефолт — имя в нижнем регистре, но реальный RootURL часто отличается ТОЛЬКО регистром
# (MobileAppReceiptScanner), и регистронезависимое сравнение считало его дефолтным.
$ru = P 'RootURL'; if ($ru -and $ru -cne $objName.ToLower()) { $dsl['rootURL'] = $ru }
Add-EnumProp 'reuseSessions' 'ReuseSessions' 'DontUse'
Add-IntProp 'sessionMaxAge' 'SessionMaxAge' 20
}
# CommonAttribute — общий реквизит: тип + value-свойства + состав объектов + свойства разделения данных. # CommonAttribute — общий реквизит: тип + value-свойства + состав объектов + свойства разделения данных.
if ($objType -eq 'CommonAttribute') { if ($objType -eq 'CommonAttribute') {
$vt = Get-TypeShorthand ($props.SelectSingleNode('md:Type', $nsm)); if ($vt -and $vt -ne 'String(0)') { $dsl['valueType'] = $vt } $vt = Get-TypeShorthand ($props.SelectSingleNode('md:Type', $nsm)); if ($vt -and $vt -ne 'String(0)') { $dsl['valueType'] = $vt }
@@ -909,7 +957,7 @@ if ($ibNode) {
if ($cl -gt 0) { $ibDef += "StandardAttribute.Code" } if ($cl -gt 0) { $ibDef += "StandardAttribute.Code" }
$ibShort = @($ibActual | ForEach-Object { Short-Field $_ }) $ibShort = @($ibActual | ForEach-Object { Short-Field $_ })
$same = ($ibShort.Count -eq $ibDef.Count) $same = ($ibShort.Count -eq $ibDef.Count)
if ($same) { for ($k = 0; $k -lt $ibShort.Count; $k++) { if ($ibShort[$k] -ne $ibDef[$k]) { $same = $false; break } } } if ($same) { for ($k = 0; $k -lt $ibShort.Count; $k++) { if ($ibShort[$k] -cne $ibDef[$k]) { $same = $false; break } } }
if (-not $same) { $dsl['inputByString'] = [System.Collections.ArrayList]@($ibShort) } if (-not $same) { $dsl['inputByString'] = [System.Collections.ArrayList]@($ibShort) }
} }
@@ -981,7 +1029,13 @@ if ($charsNode) {
filterField = Shorten-CharField (& $gt 'TypesFilterField' $ct) $tFrom filterField = Shorten-CharField (& $gt 'TypesFilterField' $ct) $tFrom
filterValue = if ($tfvNil -eq 'true') { $null } else { Convert-ChScalarNode $tfvNode } filterValue = if ($tfvNil -eq 'true') { $null } else { Convert-ChScalarNode $tfvNode }
} }
$dpf = & $giv 'DataPathField' $ct; if ($dpf -ne -1) { $types['dataPathField'] = $dpf } # DataPathField полиморфно: обычно -1, но встречается ПУТЬ к полю (8 случаев на корпус).
# Жёсткое [int] на нём роняло декомпиляцию всего объекта.
$dpfN = $ct.SelectSingleNode('xr:DataPathField', $nsm)
$dpfT = if ($dpfN) { $dpfN.InnerText } else { '' }
if ($dpfT -ne '' -and $dpfT -cne '-1') {
$types['dataPathField'] = if ($dpfT -match '^-?\d+$') { [int]$dpfT } else { Shorten-CharField $dpfT $tFrom }
}
$mvu = & $giv 'MultipleValuesUseField' $ct; if ($mvu -ne -1) { $types['multipleValuesUseField'] = $mvu } $mvu = & $giv 'MultipleValuesUseField' $ct; if ($mvu -ne -1) { $types['multipleValuesUseField'] = $mvu }
$values = [ordered]@{ $values = [ordered]@{
from = $vFrom from = $vFrom
@@ -1092,6 +1146,13 @@ if ($saNode) {
$ov['linkByType'] = [ordered]@{ dataPath = $saLbtDp.InnerText; linkItem = $li } $ov['linkByType'] = [ordered]@{ dataPath = $saLbtDp.InnerText; linkItem = $li }
} }
} }
# Формат 2.20: режим приведения типов. Компилятор выводит его сам (TransformValues, у Owner —
# Deny), поэтому захватываем только отклонение от этого правила.
$saTrmN = $sa.SelectSingleNode('xr:TypeReductionMode', $nsm)
if ($saTrmN -and $saTrmN.InnerText) {
$saTrmDef = if ($an -ceq 'Owner') { 'Deny' } else { 'TransformValues' }
if ($saTrmN.InnerText -ne $saTrmDef) { $ov['TypeReductionMode'] = $saTrmN.InnerText }
}
# Доп./опциональный реквизит (не в фикс-списке) — эмитим по присутствию даже без отклонений. # Доп./опциональный реквизит (не в фикс-списке) — эмитим по присутствию даже без отклонений.
if ($ov.Count -gt 0 -or ($stdFixed -notcontains $an)) { $saMap[$an] = $ov } if ($ov.Count -gt 0 -or ($stdFixed -notcontains $an)) { $saMap[$an] = $ov }
} }
@@ -1106,6 +1167,104 @@ if ($saNode) {
# --- ChildObjects: Attributes + TabularSections --- # --- ChildObjects: Attributes + TabularSections ---
$childObjs = $objNode.SelectSingleNode('md:ChildObjects', $nsm) $childObjs = $objNode.SelectSingleNode('md:ChildObjects', $nsm)
if ($childObjs) { if ($childObjs) {
# WebService: операции с параметрами. Строчное сокращение — только тип возврата (когда всё
# остальное дефолтно); иначе объект с nillable/transactioned/procedureName/параметрами.
$opNodes = @($childObjs.SelectNodes('md:Operation', $nsm))
if ($opNodes.Count -gt 0) {
$ops = [ordered]@{}
foreach ($op in $opNodes) {
$op_p = $op.SelectSingleNode('md:Properties', $nsm)
$opName = ($op_p.SelectSingleNode('md:Name', $nsm)).InnerText
$o = [ordered]@{}
$rt = Get-XDTOTypeValue ($op_p.SelectSingleNode('md:XDTOReturningValueType', $nsm))
if ($rt -and $rt -cne 'xs:string') { $o['returnType'] = $rt }
$nil = $op_p.SelectSingleNode('md:Nillable', $nsm)
if ($nil -and $nil.InnerText -eq 'true') { $o['nillable'] = $true }
$tr = $op_p.SelectSingleNode('md:Transactioned', $nsm)
if ($tr -and $tr.InnerText -eq 'true') { $o['transactioned'] = $true }
$pn = $op_p.SelectSingleNode('md:ProcedureName', $nsm)
if ($pn -and $pn.InnerText -cne $opName) { $o['procedureName'] = $pn.InnerText }
$dl = $op_p.SelectSingleNode('md:DataLockControlMode', $nsm)
if ($dl -and $dl.InnerText -cne 'Managed') { $o['dataLockControlMode'] = $dl.InnerText }
$osyn = Get-MLValue ($op_p.SelectSingleNode('md:Synonym', $nsm))
if ($null -ne $osyn -and "$osyn" -cne (Split-CamelWords $opName)) { $o['synonym'] = $osyn }
$ocmt = $op_p.SelectSingleNode('md:Comment', $nsm)
if ($ocmt -and $ocmt.InnerText) { $o['comment'] = $ocmt.InnerText }
$parNodes = @($op.SelectNodes('md:ChildObjects/md:Parameter', $nsm))
if ($parNodes.Count -gt 0) {
$pars = [ordered]@{}
foreach ($par in $parNodes) {
$pp = $par.SelectSingleNode('md:Properties', $nsm)
$parName = ($pp.SelectSingleNode('md:Name', $nsm)).InnerText
$po = [ordered]@{}
$pt = Get-XDTOTypeValue ($pp.SelectSingleNode('md:XDTOValueType', $nsm))
if ($pt) { $po['type'] = $pt }
$pnil = $pp.SelectSingleNode('md:Nillable', $nsm)
if ($pnil -and $pnil.InnerText -eq 'false') { $po['nillable'] = $false }
$pdir = $pp.SelectSingleNode('md:TransferDirection', $nsm)
if ($pdir -and $pdir.InnerText -cne 'In') { $po['direction'] = $pdir.InnerText }
$psyn = Get-MLValue ($pp.SelectSingleNode('md:Synonym', $nsm))
if ($null -ne $psyn -and "$psyn" -cne (Split-CamelWords $parName)) { $po['synonym'] = $psyn }
$pcmt = $pp.SelectSingleNode('md:Comment', $nsm)
if ($pcmt -and $pcmt.InnerText) { $po['comment'] = $pcmt.InnerText }
# Только тип и дефолтное остальное → строчное сокращение.
if ($po.Count -eq 1 -and $po.Contains('type')) { $pars[$parName] = $po['type'] } else { $pars[$parName] = $po }
}
$o['parameters'] = $pars
}
if ($o.Count -eq 1 -and $o.Contains('returnType')) { $ops[$opName] = $o['returnType'] } else { $ops[$opName] = $o }
}
$dsl['operations'] = $ops
}
# HTTPService: шаблоны URL и их методы. Шаблон — {template, methods{}}, метод — строка (только
# HTTP-метод, когда обработчик совпадает с авто-выводом ИмяШаблона+ИмяМетода) либо объект.
$tmplNodes = @($childObjs.SelectNodes('md:URLTemplate', $nsm))
if ($tmplNodes.Count -gt 0) {
$tmpls = [ordered]@{}
foreach ($t in $tmplNodes) {
$tp = $t.SelectSingleNode('md:Properties', $nsm)
$tName = ($tp.SelectSingleNode('md:Name', $nsm)).InnerText
$tObj = [ordered]@{}
$tTemplate = $tp.SelectSingleNode('md:Template', $nsm)
if ($tTemplate) { $tObj['template'] = $tTemplate.InnerText }
$tSyn = Get-MLValue ($tp.SelectSingleNode('md:Synonym', $nsm))
# -cne, не -ne: сравнение синонима с авто-выводом ДОЛЖНО быть регистрочувствительным,
# иначе "Post" против "post" считается совпадением и синоним теряется.
if ($null -ne $tSyn -and "$tSyn" -cne (Split-CamelWords $tName)) { $tObj['synonym'] = $tSyn }
$tCmt = $tp.SelectSingleNode('md:Comment', $nsm)
if ($tCmt -and $tCmt.InnerText) { $tObj['comment'] = $tCmt.InnerText }
$mNodes = @($t.SelectNodes('md:ChildObjects/md:Method', $nsm))
if ($mNodes.Count -gt 0) {
$methods = [ordered]@{}
foreach ($m in $mNodes) {
$mp = $m.SelectSingleNode('md:Properties', $nsm)
$mName = ($mp.SelectSingleNode('md:Name', $nsm)).InnerText
$mHttp = $mp.SelectSingleNode('md:HTTPMethod', $nsm)
$mHandler = $mp.SelectSingleNode('md:Handler', $nsm)
$mSyn = Get-MLValue ($mp.SelectSingleNode('md:Synonym', $nsm))
$mCmt = $mp.SelectSingleNode('md:Comment', $nsm)
$httpVal = if ($mHttp) { $mHttp.InnerText } else { 'GET' }
$handlerVal = if ($mHandler) { $mHandler.InnerText } else { '' }
$synDefault = ($null -eq $mSyn) -or ("$mSyn" -ceq (Split-CamelWords $mName))
$cmtEmpty = (-not $mCmt) -or (-not $mCmt.InnerText)
if ($handlerVal -ceq "$tName$mName" -and $synDefault -and $cmtEmpty) {
$methods[$mName] = $httpVal
} else {
$mo = [ordered]@{ httpMethod = $httpVal }
if ($handlerVal) { $mo['handler'] = $handlerVal }
if (-not $synDefault) { $mo['synonym'] = $mSyn }
if (-not $cmtEmpty) { $mo['comment'] = $mCmt.InnerText }
$methods[$mName] = $mo
}
}
$tObj['methods'] = $methods
}
$tmpls[$tName] = $tObj
}
$dsl['urlTemplates'] = $tmpls
}
$attrs = @($childObjs.SelectNodes('md:Attribute', $nsm)) $attrs = @($childObjs.SelectNodes('md:Attribute', $nsm))
if ($attrs.Count -gt 0) { if ($attrs.Count -gt 0) {
$arr = [System.Collections.ArrayList]@() $arr = [System.Collections.ArrayList]@()
@@ -1262,13 +1421,20 @@ if ($childObjs) {
if ($lnFvT -match 'decimal$') { $lnObj['fillValue'] = if ($lnFvN.InnerText -match '^-?\d+$') { [long]$lnFvN.InnerText } else { [double]$lnFvN.InnerText } } if ($lnFvT -match 'decimal$') { $lnObj['fillValue'] = if ($lnFvN.InnerText -match '^-?\d+$') { [long]$lnFvN.InnerText } else { [double]$lnFvN.InnerText } }
} }
} }
if ($tsSynCustom -or ($null -ne $tsTt) -or $tsCmt -or $tsFc -or $tsUse -or $lnObj.Count -gt 0 -or (-not $hasBlock)) { # Формат 2.20: длина номера строки ТЧ. Захватываем ВСЕГДА при наличии тега, а не
# omit-on-default: дефолт зависит от режима совместимости конфигурации (≤8_3_26 → 5,
# ≥8_3_27 → 9) и фиксируется платформой при создании ТЧ, так что вывести его здесь
# значило бы продублировать логику компилятора с риском разойтись. Явный захват точен.
$tsLnlN = $tsp.SelectSingleNode('md:LineNumberLength', $nsm)
$tsLnl = if ($tsLnlN -and $tsLnlN.InnerText) { [int]$tsLnlN.InnerText } else { $null }
if ($tsSynCustom -or ($null -ne $tsTt) -or $tsCmt -or $tsFc -or $tsUse -or $lnObj.Count -gt 0 -or (-not $hasBlock) -or ($null -ne $tsLnl)) {
$to = [ordered]@{} $to = [ordered]@{}
if ($tsSynCustom) { $to['synonym'] = $tsSyn } if ($tsSynCustom) { $to['synonym'] = $tsSyn }
if ($null -ne $tsTt) { $to['tooltip'] = $tsTt } if ($null -ne $tsTt) { $to['tooltip'] = $tsTt }
if ($tsCmt) { $to['comment'] = $tsCmt } if ($tsCmt) { $to['comment'] = $tsCmt }
if ($tsFc) { $to['fillChecking'] = $tsFc } if ($tsFc) { $to['fillChecking'] = $tsFc }
if ($tsUse) { $to['use'] = $tsUse } if ($tsUse) { $to['use'] = $tsUse }
if ($null -ne $tsLnl) { $to['lineNumberLength'] = $tsLnl }
if (-not $hasBlock) { $to['lineNumber'] = '' } elseif ($lnObj.Count -gt 0) { $to['lineNumber'] = $lnObj } if (-not $hasBlock) { $to['lineNumber'] = '' } elseif ($lnObj.Count -gt 0) { $to['lineNumber'] = $lnObj }
$to['attributes'] = $cols $to['attributes'] = $cols
$tsMap[$tsName] = $to $tsMap[$tsName] = $to
@@ -1353,7 +1519,12 @@ if (Test-Path -LiteralPath $predefPath) {
# Компактная строка для плоских: без узла Type (Catalog) ИЛИ с непустым типом → "(Код) Имя [Наим]: Тип". # Компактная строка для плоских: без узла Type (Catalog) ИЛИ с непустым типом → "(Код) Имя [Наим]: Тип".
# Пустой <Type/> в короткую не влезает (нужен явный маркер) → object-форма с type:''. # Пустой <Type/> в короткую не влезает (нужен явный маркер) → object-форма с type:''.
if (-not $isFolder -and $kids.Count -eq 0 -and ($null -eq $typeStr -or $typeStr -ne '')) { # Сокращение неоднозначно, если значение содержит собственные разделители грамматики
# "(Код) Имя [Наим]: Тип": ')' или ':' в коде, пробел/скобка/':' в имени, скобки в наименовании.
# Компилятор разбирает код как [^)]*, а имя как \S+ — на "114 (108)" разбор рассыпается,
# и элемент терял и имя, и код (6 элементов в БП и столько же в ERP).
$ambiguous = ($code -match '[):]') -or ($name -match '[\s:\[\]()]') -or ($desc -match '[\[\]]')
if (-not $isFolder -and $kids.Count -eq 0 -and ($null -eq $typeStr -or $typeStr -ne '') -and -not $ambiguous) {
$s = if ($code) { "($code) $name" } else { $name } $s = if ($code) { "($code) $name" } else { $name }
if ($desc -eq '') { $s = "$s []" } if ($desc -eq '') { $s = "$s []" }
elseif ($desc -cne $auto) { $s = "$s [$desc]" } elseif ($desc -cne $auto) { $s = "$s [$desc]" }
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# meta-decompile v0.54 — XML объекта метаданных 1С → JSON-черновик формата meta-compile # meta-decompile v0.63 — XML объекта метаданных 1С → JSON-черновик формата meta-compile
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# #
# Зеркало meta-decompile.ps1 (КАНОН). Структура 1:1 — те же имена функций, порядок, комментарии. # Зеркало meta-decompile.ps1 (КАНОН). Структура 1:1 — те же имена функций, порядок, комментарии.
@@ -58,6 +58,14 @@ def _text(node):
return ''.join(node.itertext()) return ''.join(node.itertext())
def _ns_of_prefix(node, prefix):
"""URI по префиксу — зеркало GetNamespaceOfPrefix из PS. lxml отдаёт nsmap с учётом
унаследованных объявлений, поэтому локальный xmlns:dNpM на самом теге тоже виден."""
if node is None:
return None
return node.nsmap.get(prefix)
def _attr(node, name, ns=None): def _attr(node, name, ns=None):
"""GetAttribute(name[, ns]) — .NET возвращает '' для отсутствующего атрибута, lxml → None.""" """GetAttribute(name[, ns]) — .NET возвращает '' для отсутствующего атрибута, lxml → None."""
if node is None: if node is None:
@@ -290,7 +298,30 @@ def get_type_shorthand(type_node):
# Скалярное значение параметра выбора (<Value xsi:type=...>) → JSON-значение (bool/число/строка). # Скалярное значение параметра выбора (<Value xsi:type=...>) → JSON-значение (bool/число/строка).
def get_xdto_type_value(node):
"""XDTO-тип: префиксное значение (d6p1:Local) -> нотация Кларка "{uri}Local".
Префиксы платформы (dNpM) произвольны и переносу не подлежат; стандартные (xs:, v8:, xr:)
оставляем дословно компилятор их так и пишет.
"""
if node is None:
return None
txt = _text(node)
m = re.match(r'^([\w.\-]+):(.+)$', txt or '')
if m:
prefix, local = m.group(1), m.group(2)
if prefix not in ('xs', 'xsi', 'v8', 'xr'):
uri = _ns_of_prefix(node, prefix)
if uri:
return '{%s}%s' % (uri, local)
return txt
def convert_ch_scalar_node(vN): def convert_ch_scalar_node(vN):
# nil-элемент массива (<v8:Value xsi:nil="true"/>) -> JSON null. Без этого он приезжал пустой
# строкой и компилятор эмитил xs:string вместо nil.
if _attr(vN, 'nil', NS_XSI) == 'true':
return None
xt = _attr(vN, 'type', NS_XSI) xt = _attr(vN, 'type', NS_XSI)
txt = _text(vN) txt = _text(vN)
if re.search(r'boolean$', xt, re.I): if re.search(r'boolean$', xt, re.I):
@@ -299,6 +330,10 @@ def convert_ch_scalar_node(vN):
if re.match(r'^-?\d+$', txt): if re.match(r'^-?\d+$', txt):
return int(txt) return int(txt)
return float(txt) return float(txt)
# Пустой DesignTimeRef != пустая строка: без маркера тип терялся, и компилятор эмитил xs:string.
# Та же конвенция, что у fillValue — маркер emptyRef.
if re.search(r'DesignTimeRef$', xt, re.I) and txt == '':
return {'emptyRef': True}
return txt return txt
@@ -456,6 +491,11 @@ def attr_to_dsl(attr_node):
v = en('UseInTotals') v = en('UseInTotals')
if v == 'false': if v == 'false':
extra['useInTotals'] = False # дефолт true → захват при false extra['useInTotals'] = False # дефолт true → захват при false
# Формат 2.20: режим приведения типов измерения РС. Дефолт TransformValues (его компилятор
# эмитит сам) → захватываем только отклонение.
v = en('TypeReductionMode')
if v and v != 'TransformValues':
extra['typeReductionMode'] = v
v = en('BaseDimension') v = en('BaseDimension')
if v == 'true': if v == 'true':
extra['baseDimension'] = True extra['baseDimension'] = True
@@ -677,7 +717,11 @@ def predef_item_to_dsl(item_el):
auto = split_camel_words(name) auto = split_camel_words(name)
# Компактная строка для плоских: без узла Type (Catalog) ИЛИ с непустым типом → "(Код) Имя [Наим]: Тип". # Компактная строка для плоских: без узла Type (Catalog) ИЛИ с непустым типом → "(Код) Имя [Наим]: Тип".
if (not is_folder) and len(kids) == 0 and (type_str is None or type_str != ''): # Сокращение неоднозначно, если значение содержит собственные разделители грамматики:
# ')' или ':' в коде, пробел/скобка/':' в имени, скобки в наименовании. Компилятор читает код
# как [^)]*, а имя как \S+ — на "114 (108)" разбор рассыпается и элемент терял имя и код.
ambiguous = bool(re.search(r'[):]', code or '')) or bool(re.search(r'[\s:\[\]()]', name or '')) or bool(re.search(r'[\[\]]', desc or ''))
if (not is_folder) and len(kids) == 0 and (type_str is None or type_str != '') and not ambiguous:
s = ("(%s) %s" % (code, name)) if code else name s = ("(%s) %s" % (code, name)) if code else name
if desc == '': if desc == '':
s = s + " []" s = s + " []"
@@ -1178,6 +1222,28 @@ def build_dsl():
get_picture_to_dsl(props, dsl) get_picture_to_dsl(props, dsl)
add_enum_prop('category', 'Category', 'NavigationPanel') add_enum_prop('category', 'Category', 'NavigationPanel')
# CommonCommand. # CommonCommand.
# WebService — пространство имён, состав XDTO-пакетов, дескриптор.
if obj_type == 'WebService':
ns_ = P('Namespace')
if ns_:
dsl['namespace'] = ns_
pkg_nodes = _nodes(props, 'md:XDTOPackages/xr:Item/xr:Value')
if len(pkg_nodes) > 0:
dsl['xdtoPackages'] = [_text(pn) for pn in pkg_nodes]
dfn = P('DescriptorFileName')
if dfn and dfn != f'{obj_name}.1cws':
dsl['descriptorFileName'] = dfn
add_enum_prop('reuseSessions', 'ReuseSessions', 'DontUse')
add_int_prop('sessionMaxAge', 'SessionMaxAge', 20)
# HTTPService — корневой URL, повторное использование сеансов, время жизни сеанса.
if obj_type == 'HTTPService':
ru = P('RootURL')
# Сравнение регистрочувствительное: реальный RootURL часто отличается от дефолта
# только регистром (MobileAppReceiptScanner).
if ru and ru != obj_name.lower():
dsl['rootURL'] = ru
add_enum_prop('reuseSessions', 'ReuseSessions', 'DontUse')
add_int_prop('sessionMaxAge', 'SessionMaxAge', 20)
if obj_type == 'CommonCommand': if obj_type == 'CommonCommand':
grp = P('Group') grp = P('Group')
if grp: if grp:
@@ -1453,9 +1519,13 @@ def build_dsl():
'filterField': shorten_char_field(gt('TypesFilterField', ct), t_from), 'filterField': shorten_char_field(gt('TypesFilterField', ct), t_from),
'filterValue': None if tfv_nil == 'true' else convert_ch_scalar_node(tfv_node), 'filterValue': None if tfv_nil == 'true' else convert_ch_scalar_node(tfv_node),
} }
dpf = giv('DataPathField', ct) # DataPathField полиморфно: обычно -1, но встречается ПУТЬ к полю (8 случаев на
if dpf != -1: # корпус). Жёсткое int() на нём роняло декомпиляцию всего объекта.
types['dataPathField'] = dpf dpf_node = _lx1(ct, "*[local-name()='DataPathField']")
dpf_txt = _text(dpf_node) if dpf_node is not None else ''
if dpf_txt != '' and dpf_txt != '-1':
types['dataPathField'] = (int(dpf_txt) if re.fullmatch(r'-?\d+', dpf_txt)
else shorten_char_field(dpf_txt, t_from))
mvu = giv('MultipleValuesUseField', ct) mvu = giv('MultipleValuesUseField', ct)
if mvu != -1: if mvu != -1:
types['multipleValuesUseField'] = mvu types['multipleValuesUseField'] = mvu
@@ -1587,6 +1657,13 @@ def build_dsl():
li = int(_text(sa_lbt_li)) if (sa_lbt_li is not None and _text(sa_lbt_li)) else 0 li = int(_text(sa_lbt_li)) if (sa_lbt_li is not None and _text(sa_lbt_li)) else 0
ov['linkByType'] = {'dataPath': _text(sa_lbt_dp), 'linkItem': li} ov['linkByType'] = {'dataPath': _text(sa_lbt_dp), 'linkItem': li}
# Доп./опциональный реквизит (не в фикс-списке) — эмитим по присутствию даже без отклонений. # Доп./опциональный реквизит (не в фикс-списке) — эмитим по присутствию даже без отклонений.
# Формат 2.20: режим приведения типов. Компилятор выводит его сам (TransformValues,
# у Owner — Deny), поэтому захватываем только отклонение от этого правила.
sa_trm_n = _single(sa, 'xr:TypeReductionMode')
if sa_trm_n is not None and (sa_trm_n.text or '').strip():
sa_trm_def = 'Deny' if an == 'Owner' else 'TransformValues'
if sa_trm_n.text.strip() != sa_trm_def:
ov['TypeReductionMode'] = sa_trm_n.text.strip()
if len(ov) > 0 or (an not in std_fixed): if len(ov) > 0 or (an not in std_fixed):
sa_map[an] = ov sa_map[an] = ov
if len(sa_map) > 0 or (obj_type in std_conditional_types): if len(sa_map) > 0 or (obj_type in std_conditional_types):
@@ -1598,6 +1675,100 @@ def build_dsl():
# --- ChildObjects: Attributes + TabularSections --- # --- ChildObjects: Attributes + TabularSections ---
child_objs = _single(obj_node, 'md:ChildObjects') child_objs = _single(obj_node, 'md:ChildObjects')
if child_objs is not None: if child_objs is not None:
# WebService: операции с параметрами. Строчное сокращение — только тип возврата.
op_nodes = _nodes(child_objs, 'md:Operation')
if len(op_nodes) > 0:
ops = {}
for op in op_nodes:
op_p = _single(op, 'md:Properties')
op_name = _text(_single(op_p, 'md:Name'))
o = {}
rt = get_xdto_type_value(_single(op_p, 'md:XDTOReturningValueType'))
if rt and rt != 'xs:string':
o['returnType'] = rt
if _text(_single(op_p, 'md:Nillable')) == 'true':
o['nillable'] = True
if _text(_single(op_p, 'md:Transactioned')) == 'true':
o['transactioned'] = True
pn = _text(_single(op_p, 'md:ProcedureName'))
if pn and pn != op_name:
o['procedureName'] = pn
dl = _text(_single(op_p, 'md:DataLockControlMode'))
if dl and dl != 'Managed':
o['dataLockControlMode'] = dl
osyn = get_ml_value(_single(op_p, 'md:Synonym'))
if osyn is not None and str(osyn) != split_camel_words(op_name):
o['synonym'] = osyn
ocmt = _text(_single(op_p, 'md:Comment'))
if ocmt:
o['comment'] = ocmt
par_nodes = _nodes(op, 'md:ChildObjects/md:Parameter')
if len(par_nodes) > 0:
pars = {}
for par in par_nodes:
pp = _single(par, 'md:Properties')
par_name = _text(_single(pp, 'md:Name'))
po = {}
pt = get_xdto_type_value(_single(pp, 'md:XDTOValueType'))
if pt:
po['type'] = pt
if _text(_single(pp, 'md:Nillable')) == 'false':
po['nillable'] = False
pdir = _text(_single(pp, 'md:TransferDirection'))
if pdir and pdir != 'In':
po['direction'] = pdir
psyn = get_ml_value(_single(pp, 'md:Synonym'))
if psyn is not None and str(psyn) != split_camel_words(par_name):
po['synonym'] = psyn
pcmt = _text(_single(pp, 'md:Comment'))
if pcmt:
po['comment'] = pcmt
pars[par_name] = po['type'] if list(po.keys()) == ['type'] else po
o['parameters'] = pars
ops[op_name] = o['returnType'] if list(o.keys()) == ['returnType'] else o
dsl['operations'] = ops
# HTTPService: шаблоны URL и их методы.
tmpl_nodes = _nodes(child_objs, 'md:URLTemplate')
if len(tmpl_nodes) > 0:
tmpls = {}
for t in tmpl_nodes:
tp = _single(t, 'md:Properties')
t_name = _text(_single(tp, 'md:Name'))
t_obj = {}
t_template = _text(_single(tp, 'md:Template'))
if t_template:
t_obj['template'] = t_template
t_syn = get_ml_value(_single(tp, 'md:Synonym'))
if t_syn is not None and str(t_syn) != split_camel_words(t_name):
t_obj['synonym'] = t_syn
t_cmt = _text(_single(tp, 'md:Comment'))
if t_cmt:
t_obj['comment'] = t_cmt
m_nodes = _nodes(t, 'md:ChildObjects/md:Method')
if len(m_nodes) > 0:
methods = {}
for m in m_nodes:
mp = _single(m, 'md:Properties')
m_name = _text(_single(mp, 'md:Name'))
http_val = _text(_single(mp, 'md:HTTPMethod')) or 'GET'
handler_val = _text(_single(mp, 'md:Handler')) or ''
m_syn = get_ml_value(_single(mp, 'md:Synonym'))
m_cmt = _text(_single(mp, 'md:Comment'))
syn_default = m_syn is None or str(m_syn) == split_camel_words(m_name)
if handler_val == f'{t_name}{m_name}' and syn_default and not m_cmt:
methods[m_name] = http_val
else:
mo = {'httpMethod': http_val}
if handler_val:
mo['handler'] = handler_val
if not syn_default:
mo['synonym'] = m_syn
if m_cmt:
mo['comment'] = m_cmt
methods[m_name] = mo
t_obj['methods'] = methods
tmpls[t_name] = t_obj
dsl['urlTemplates'] = tmpls
attrs = _nodes(child_objs, 'md:Attribute') attrs = _nodes(child_objs, 'md:Attribute')
if len(attrs) > 0: if len(attrs) > 0:
arr = [] arr = []
@@ -1782,7 +1953,13 @@ def build_dsl():
ln_fv_t = _attr(ln_fv_n, 'type', NS_XSI) ln_fv_t = _attr(ln_fv_n, 'type', NS_XSI)
if re.search(r'decimal$', ln_fv_t, re.I): if re.search(r'decimal$', ln_fv_t, re.I):
ln_obj['fillValue'] = int(_text(ln_fv_n)) if re.match(r'^-?\d+$', _text(ln_fv_n)) else float(_text(ln_fv_n)) ln_obj['fillValue'] = int(_text(ln_fv_n)) if re.match(r'^-?\d+$', _text(ln_fv_n)) else float(_text(ln_fv_n))
if ts_syn_custom or (ts_tt is not None) or ts_cmt or ts_fc or ts_use or len(ln_obj) > 0 or (not has_block): # Формат 2.20: длина номера строки ТЧ. Захватываем ВСЕГДА при наличии тега, а не
# omit-on-default: дефолт зависит от режима совместимости конфигурации (<=8_3_26 → 5,
# >=8_3_27 → 9) и фиксируется платформой при создании ТЧ, так что вывести его здесь
# значило бы продублировать логику компилятора с риском разойтись. Явный захват точен.
ts_lnl_n = _single(tsp, 'md:LineNumberLength')
ts_lnl = int(ts_lnl_n.text) if ts_lnl_n is not None and (ts_lnl_n.text or '').strip() else None
if ts_syn_custom or (ts_tt is not None) or ts_cmt or ts_fc or ts_use or len(ln_obj) > 0 or (not has_block) or (ts_lnl is not None):
to = {} to = {}
if ts_syn_custom: if ts_syn_custom:
to['synonym'] = ts_syn to['synonym'] = ts_syn
@@ -1794,6 +1971,8 @@ def build_dsl():
to['fillChecking'] = ts_fc to['fillChecking'] = ts_fc
if ts_use: if ts_use:
to['use'] = ts_use to['use'] = ts_use
if ts_lnl is not None:
to['lineNumberLength'] = ts_lnl
if not has_block: if not has_block:
to['lineNumber'] = '' to['lineNumber'] = ''
elif len(ln_obj) > 0: elif len(ln_obj) > 0:
@@ -1910,7 +2089,7 @@ SUPPORTED_TYPES = (
'Enum', 'Report', 'DataProcessor', 'Constant', 'DefinedType', 'FunctionalOption', 'DocumentJournal', 'Sequence', 'Enum', 'Report', 'DataProcessor', 'Constant', 'DefinedType', 'FunctionalOption', 'DocumentJournal', 'Sequence',
'FilterCriterion', 'DocumentNumerator', 'SettingsStorage', 'CommonModule', 'EventSubscription', 'ScheduledJob', 'FilterCriterion', 'DocumentNumerator', 'SettingsStorage', 'CommonModule', 'EventSubscription', 'ScheduledJob',
'CommonForm', 'SessionParameter', 'CommonCommand', 'CommandGroup', 'CommonAttribute', 'FunctionalOptionsParameter', 'CommonForm', 'SessionParameter', 'CommonCommand', 'CommandGroup', 'CommonAttribute', 'FunctionalOptionsParameter',
'WSReference', 'CommonPicture', 'CommonTemplate', 'WSReference', 'CommonPicture', 'CommonTemplate', 'HTTPService', 'WebService',
) )
+2 -2
View File
@@ -18,13 +18,13 @@ allowed-tools:
### Inline mode (простые операции) ### Inline mode (простые операции)
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-edit.ps1" -ObjectPath "<path>" -Operation <op> -Value "<val>" python "${CLAUDE_SKILL_DIR}/scripts/meta-edit.py" -ObjectPath "<path>" -Operation <op> -Value "<val>"
``` ```
### JSON mode (сложные/комбинированные) ### JSON mode (сложные/комбинированные)
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-edit.ps1" -DefinitionFile "<json>" -ObjectPath "<path>" python "${CLAUDE_SKILL_DIR}/scripts/meta-edit.py" -DefinitionFile "<json>" -ObjectPath "<path>"
``` ```
| Параметр | Описание | | Параметр | Описание |
+1 -1
View File
@@ -3,7 +3,7 @@
Для сложных и комбинированных операций используйте JSON-файл вместо inline-режима. Для сложных и комбинированных операций используйте JSON-файл вместо inline-режима.
```powershell ```powershell
powershell.exe -NoProfile -File .claude/skills/meta-edit/scripts/meta-edit.ps1 -DefinitionFile "<json>" -ObjectPath "<path>" python .claude/skills/meta-edit/scripts/meta-edit.py -DefinitionFile "<json>" -ObjectPath "<path>"
``` ```
## add — добавить элементы ## add — добавить элементы

Some files were not shown because too many files have changed in this diff Show More