Compare commits

..

1 Commits

Author SHA1 Message Date
github-actions[bot] 2922b55db4 Auto-build: claude-code (python) from fc2323f 2026-07-18 10:24:07 +00:00
2548 changed files with 76642 additions and 237971 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'
``` ```
## Операции ## Операции
+17 -3
View File
@@ -1,4 +1,4 @@
# cf-edit v1.7 — Edit 1C configuration root (Configuration.xml) # cf-edit v1.9 — Edit 1C configuration root (Configuration.xml)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)][Alias('Path')][string]$ConfigPath, [Parameter(Mandatory)][Alias('Path')][string]$ConfigPath,
@@ -44,6 +44,16 @@ function Get-RootUuid([string]$xmlPath) {
} catch {} } catch {}
return $null return $null
} }
function Test-ExternalObjectRoot([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $false }
try {
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
$el = $mx.DocumentElement.FirstChild
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
} catch {}
return $false
}
function Find-V8Project([string]$startDir) { function Find-V8Project([string]$startDir) {
$d = $startDir $d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) { for ($i = 0; $i -lt 20 -and $d; $i++) {
@@ -80,10 +90,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
try { try {
$rp = $targetPath $rp = $targetPath
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {} try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
if (Test-ExternalObjectRoot $rp) { return }
$elemUuid = Get-RootUuid $rp $elemUuid = Get-RootUuid $rp
$cfgDir = $null; $binPath = $null $cfgDir = $null; $binPath = $null
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) } $d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
for ($i = 0; $i -lt 12 -and $d; $i++) { for ($i = 0; $i -lt 12 -and $d; $i++) {
if (Test-ExternalObjectRoot "$d.xml") { return }
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" } if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $cfgDir) { if (-not $cfgDir) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
@@ -196,7 +209,7 @@ Info "Configuration: $($script:objName)"
$script:typeOrder = @( $script:typeOrder = @(
"Language","Subsystem","StyleItem","Style", "Language","Subsystem","StyleItem","Style",
"CommonPicture","SessionParameter","Role","CommonTemplate", "CommonPicture","SessionParameter","Role","CommonTemplate",
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan", "FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
"XDTOPackage","WebService","HTTPService","WSReference", "XDTOPackage","WebService","HTTPService","WSReference",
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption", "EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup", "FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
@@ -212,7 +225,7 @@ $script:typeOrder = @(
$script:typeToDir = @{ $script:typeToDir = @{
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles" "Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"; "CommonTemplate"="CommonTemplates" "CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"; "CommonTemplate"="CommonTemplates"
"FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"; "CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans" "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"; "Bot"="Bots"; "CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"
"XDTOPackage"="XDTOPackages"; "WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences" "XDTOPackage"="XDTOPackages"; "WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"; "SettingsStorage"="SettingsStorages"; "FunctionalOption"="FunctionalOptions" "EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"; "SettingsStorage"="SettingsStorages"; "FunctionalOption"="FunctionalOptions"
"FunctionalOptionsParameter"="FunctionalOptionsParameters"; "DefinedType"="DefinedTypes"; "CommonCommand"="CommonCommands"; "CommandGroup"="CommandGroups" "FunctionalOptionsParameter"="FunctionalOptionsParameters"; "DefinedType"="DefinedTypes"; "CommonCommand"="CommonCommands"; "CommandGroup"="CommandGroups"
@@ -700,6 +713,7 @@ $script:ruTypeMap = @{
"регистррасчёта" = "CalculationRegister" "регистррасчёта" = "CalculationRegister"
"бизнеспроцесс" = "BusinessProcess" "бизнеспроцесс" = "BusinessProcess"
"задача" = "Task" "задача" = "Task"
"бот" = "Bot"
"планобмена" = "ExchangePlan" "планобмена" = "ExchangePlan"
"хранилищенастроек" = "SettingsStorage" "хранилищенастроек" = "SettingsStorage"
} }
+21 -3
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# cf-edit v1.7 — Edit 1C configuration root (Configuration.xml) # cf-edit v1.9 — Edit 1C configuration root (Configuration.xml)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -33,6 +33,18 @@ def _sg_root_uuid(xml_path):
return None return None
def _sg_is_external_root(xml_path):
if not os.path.isfile(xml_path):
return False
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str):
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
except Exception:
return False
return False
def _sg_find_v8project(start_dir): def _sg_find_v8project(start_dir):
d = start_dir d = start_dir
for _ in range(20): for _ in range(20):
@@ -72,6 +84,9 @@ def _sg_get_edit_mode(cfg_dir):
def assert_edit_allowed(target_path, require): def assert_edit_allowed(target_path, require):
try: try:
rp = os.path.abspath(target_path) rp = os.path.abspath(target_path)
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
if _sg_is_external_root(rp):
return
elem_uuid = _sg_root_uuid(rp) elem_uuid = _sg_root_uuid(rp)
cfg_dir = None cfg_dir = None
bin_path = None bin_path = None
@@ -79,6 +94,8 @@ def assert_edit_allowed(target_path, require):
for _ in range(12): for _ in range(12):
if not d: if not d:
break break
if _sg_is_external_root(d + ".xml"):
return
if not elem_uuid: if not elem_uuid:
elem_uuid = _sg_root_uuid(d + ".xml") elem_uuid = _sg_root_uuid(d + ".xml")
if not cfg_dir: if not cfg_dir:
@@ -182,7 +199,7 @@ XS_NS = "http://www.w3.org/2001/XMLSchema"
TYPE_ORDER = [ TYPE_ORDER = [
"Language", "Subsystem", "StyleItem", "Style", "Language", "Subsystem", "StyleItem", "Style",
"CommonPicture", "SessionParameter", "Role", "CommonTemplate", "CommonPicture", "SessionParameter", "Role", "CommonTemplate",
"FilterCriterion", "CommonModule", "CommonAttribute", "ExchangePlan", "FilterCriterion", "CommonModule", "Bot", "CommonAttribute", "ExchangePlan",
"XDTOPackage", "WebService", "HTTPService", "WSReference", "XDTOPackage", "WebService", "HTTPService", "WSReference",
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption", "EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup", "FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
@@ -198,7 +215,7 @@ TYPE_ORDER = [
TYPE_TO_DIR = { TYPE_TO_DIR = {
"Language": "Languages", "Subsystem": "Subsystems", "StyleItem": "StyleItems", "Style": "Styles", "Language": "Languages", "Subsystem": "Subsystems", "StyleItem": "StyleItems", "Style": "Styles",
"CommonPicture": "CommonPictures", "SessionParameter": "SessionParameters", "Role": "Roles", "CommonTemplate": "CommonTemplates", "CommonPicture": "CommonPictures", "SessionParameter": "SessionParameters", "Role": "Roles", "CommonTemplate": "CommonTemplates",
"FilterCriterion": "FilterCriteria", "CommonModule": "CommonModules", "CommonAttribute": "CommonAttributes", "ExchangePlan": "ExchangePlans", "FilterCriterion": "FilterCriteria", "CommonModule": "CommonModules", "Bot": "Bots", "CommonAttribute": "CommonAttributes", "ExchangePlan": "ExchangePlans",
"XDTOPackage": "XDTOPackages", "WebService": "WebServices", "HTTPService": "HTTPServices", "WSReference": "WSReferences", "XDTOPackage": "XDTOPackages", "WebService": "WebServices", "HTTPService": "HTTPServices", "WSReference": "WSReferences",
"EventSubscription": "EventSubscriptions", "ScheduledJob": "ScheduledJobs", "SettingsStorage": "SettingsStorages", "FunctionalOption": "FunctionalOptions", "EventSubscription": "EventSubscriptions", "ScheduledJob": "ScheduledJobs", "SettingsStorage": "SettingsStorages", "FunctionalOption": "FunctionalOptions",
"FunctionalOptionsParameter": "FunctionalOptionsParameters", "DefinedType": "DefinedTypes", "CommonCommand": "CommonCommands", "CommandGroup": "CommandGroups", "FunctionalOptionsParameter": "FunctionalOptionsParameters", "DefinedType": "DefinedTypes", "CommonCommand": "CommonCommands", "CommandGroup": "CommandGroups",
@@ -771,6 +788,7 @@ def main():
"регистррасчета": "CalculationRegister", "регистррасчета": "CalculationRegister",
"регистррасчёта": "CalculationRegister", "регистррасчёта": "CalculationRegister",
"бизнеспроцесс": "BusinessProcess", "бизнеспроцесс": "BusinessProcess",
"бот": "Bot",
"задача": "Task", "планобмена": "ExchangePlan", "задача": "Task", "планобмена": "ExchangePlan",
"хранилищенастроек": "SettingsStorage", "хранилищенастроек": "SettingsStorage",
} }
+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 "<путь>"
``` ```
## Три режима ## Три режима
+3 -2
View File
@@ -1,4 +1,4 @@
# cf-info v1.3 — Compact summary of 1C configuration root # cf-info v1.4 — Compact summary of 1C configuration root
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory=$true)][Alias('Path')][string]$ConfigPath, [Parameter(Mandatory=$true)][Alias('Path')][string]$ConfigPath,
@@ -89,7 +89,7 @@ function Get-PropML([string]$propName) {
$typeOrder = @( $typeOrder = @(
"Language","Subsystem","StyleItem","Style", "Language","Subsystem","StyleItem","Style",
"CommonPicture","SessionParameter","Role","CommonTemplate", "CommonPicture","SessionParameter","Role","CommonTemplate",
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan", "FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
"XDTOPackage","WebService","HTTPService","WSReference", "XDTOPackage","WebService","HTTPService","WSReference",
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption", "EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup", "FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
@@ -105,6 +105,7 @@ $typeRuNames = @{
"Language"="Языки"; "Subsystem"="Подсистемы"; "StyleItem"="Элементы стиля"; "Style"="Стили" "Language"="Языки"; "Subsystem"="Подсистемы"; "StyleItem"="Элементы стиля"; "Style"="Стили"
"CommonPicture"="Общие картинки"; "SessionParameter"="Параметры сеанса"; "Role"="Роли" "CommonPicture"="Общие картинки"; "SessionParameter"="Параметры сеанса"; "Role"="Роли"
"CommonTemplate"="Общие макеты"; "FilterCriterion"="Критерии отбора"; "CommonModule"="Общие модули" "CommonTemplate"="Общие макеты"; "FilterCriterion"="Критерии отбора"; "CommonModule"="Общие модули"
"Bot"="Боты"
"CommonAttribute"="Общие реквизиты"; "ExchangePlan"="Планы обмена"; "XDTOPackage"="XDTO-пакеты" "CommonAttribute"="Общие реквизиты"; "ExchangePlan"="Планы обмена"; "XDTOPackage"="XDTO-пакеты"
"WebService"="Веб-сервисы"; "HTTPService"="HTTP-сервисы"; "WSReference"="WS-ссылки" "WebService"="Веб-сервисы"; "HTTPService"="HTTP-сервисы"; "WSReference"="WS-ссылки"
"EventSubscription"="Подписки на события"; "ScheduledJob"="Регламентные задания" "EventSubscription"="Подписки на события"; "ScheduledJob"="Регламентные задания"
+3 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# cf-info v1.3 — Compact summary of 1C configuration root # cf-info v1.4 — Compact summary of 1C configuration root
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -95,7 +95,7 @@ def get_prop_ml(prop_name):
type_order = [ type_order = [
"Language", "Subsystem", "StyleItem", "Style", "Language", "Subsystem", "StyleItem", "Style",
"CommonPicture", "SessionParameter", "Role", "CommonTemplate", "CommonPicture", "SessionParameter", "Role", "CommonTemplate",
"FilterCriterion", "CommonModule", "CommonAttribute", "ExchangePlan", "FilterCriterion", "CommonModule", "Bot", "CommonAttribute", "ExchangePlan",
"XDTOPackage", "WebService", "HTTPService", "WSReference", "XDTOPackage", "WebService", "HTTPService", "WSReference",
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption", "EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup", "FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
@@ -111,6 +111,7 @@ type_ru_names = {
"Language": "Языки", "Subsystem": "Подсистемы", "StyleItem": "Элементы стиля", "Style": "Стили", "Language": "Языки", "Subsystem": "Подсистемы", "StyleItem": "Элементы стиля", "Style": "Стили",
"CommonPicture": "Общие картинки", "SessionParameter": "Параметры сеанса", "Role": "Роли", "CommonPicture": "Общие картинки", "SessionParameter": "Параметры сеанса", "Role": "Роли",
"CommonTemplate": "Общие макеты", "FilterCriterion": "Критерии отбора", "CommonModule": "Общие модули", "CommonTemplate": "Общие макеты", "FilterCriterion": "Критерии отбора", "CommonModule": "Общие модули",
"Bot": "Боты",
"CommonAttribute": "Общие реквизиты", "ExchangePlan": "Планы обмена", "XDTOPackage": "XDTO-пакеты", "CommonAttribute": "Общие реквизиты", "ExchangePlan": "Планы обмена", "XDTOPackage": "XDTO-пакеты",
"WebService": "Веб-сервисы", "HTTPService": "HTTP-сервисы", "WSReference": "WS-ссылки", "WebService": "Веб-сервисы", "HTTPService": "HTTP-сервисы", "WSReference": "WS-ссылки",
"EventSubscription": "Подписки на события", "ScheduledJob": "Регламентные задания", "EventSubscription": "Подписки на события", "ScheduledJob": "Регламентные задания",
+1 -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 "МояКонфигурация"
``` ```
## Примеры ## Примеры
+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.3 — Validate 1C configuration root structure # cf-validate v1.4 — Validate 1C configuration root structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -104,11 +104,11 @@ $validClassIds = @(
"fb282519-d103-4dd3-bc12-cb271d631dfc" # home page / client app interface "fb282519-d103-4dd3-bc12-cb271d631dfc" # home page / client app interface
) )
# 44 types in canonical order # 45 types in canonical order
$childObjectTypes = @( $childObjectTypes = @(
"Language","Subsystem","StyleItem","Style", "Language","Subsystem","StyleItem","Style",
"CommonPicture","SessionParameter","Role","CommonTemplate", "CommonPicture","SessionParameter","Role","CommonTemplate",
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan", "FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
"XDTOPackage","WebService","HTTPService","WSReference", "XDTOPackage","WebService","HTTPService","WSReference",
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption", "EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup", "FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
@@ -125,6 +125,7 @@ $childTypeDirMap = @{
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles" "Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles" "CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"
"CommonTemplate"="CommonTemplates"; "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules" "CommonTemplate"="CommonTemplates"; "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"
"Bot"="Bots"
"CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"; "XDTOPackage"="XDTOPackages" "CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"; "XDTOPackage"="XDTOPackages"
"WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences" "WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs" "EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# cf-validate v1.3 — Validate 1C configuration XML structure # cf-validate v1.4 — Validate 1C configuration XML structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Validates Configuration.xml: root structure, InternalInfo, properties, ChildObjects, languages.""" """Validates Configuration.xml: root structure, InternalInfo, properties, ChildObjects, languages."""
import sys, os, argparse, re import sys, os, argparse, re
@@ -33,11 +33,11 @@ VALID_CLASS_IDS = [
'fb282519-d103-4dd3-bc12-cb271d631dfc', # home page / client app interface 'fb282519-d103-4dd3-bc12-cb271d631dfc', # home page / client app interface
] ]
# 44 types in canonical order # 45 types in canonical order
CHILD_OBJECT_TYPES = [ CHILD_OBJECT_TYPES = [
'Language', 'Subsystem', 'StyleItem', 'Style', 'Language', 'Subsystem', 'StyleItem', 'Style',
'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate', 'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate',
'FilterCriterion', 'CommonModule', 'CommonAttribute', 'ExchangePlan', 'FilterCriterion', 'CommonModule', 'Bot', 'CommonAttribute', 'ExchangePlan',
'XDTOPackage', 'WebService', 'HTTPService', 'WSReference', 'XDTOPackage', 'WebService', 'HTTPService', 'WSReference',
'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption', 'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption',
'FunctionalOptionsParameter', 'DefinedType', 'CommonCommand', 'CommandGroup', 'FunctionalOptionsParameter', 'DefinedType', 'CommonCommand', 'CommandGroup',
@@ -54,6 +54,7 @@ CHILD_TYPE_DIR_MAP = {
'Language': 'Languages', 'Subsystem': 'Subsystems', 'StyleItem': 'StyleItems', 'Style': 'Styles', 'Language': 'Languages', 'Subsystem': 'Subsystems', 'StyleItem': 'StyleItems', 'Style': 'Styles',
'CommonPicture': 'CommonPictures', 'SessionParameter': 'SessionParameters', 'Role': 'Roles', 'CommonPicture': 'CommonPictures', 'SessionParameter': 'SessionParameters', 'Role': 'Roles',
'CommonTemplate': 'CommonTemplates', 'FilterCriterion': 'FilterCriteria', 'CommonModule': 'CommonModules', 'CommonTemplate': 'CommonTemplates', 'FilterCriterion': 'FilterCriteria', 'CommonModule': 'CommonModules',
'Bot': 'Bots',
'CommonAttribute': 'CommonAttributes', 'ExchangePlan': 'ExchangePlans', 'XDTOPackage': 'XDTOPackages', 'CommonAttribute': 'CommonAttributes', 'ExchangePlan': 'ExchangePlans', 'XDTOPackage': 'XDTOPackages',
'WebService': 'WebServices', 'HTTPService': 'HTTPServices', 'WSReference': 'WSReferences', 'WebService': 'WebServices', 'HTTPService': 'HTTPServices', 'WSReference': 'WSReferences',
'EventSubscription': 'EventSubscriptions', 'ScheduledJob': 'ScheduledJobs', 'EventSubscription': 'EventSubscriptions', 'ScheduledJob': 'ScheduledJobs',
+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.Контрагенты"
``` ```
## Примеры ## Примеры
+206 -112
View File
@@ -1,4 +1,4 @@
# cfe-borrow v1.3 — Borrow objects from configuration into extension (CFE) # cfe-borrow v1.8 — 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,
@@ -13,6 +13,31 @@ $ErrorActionPreference = "Stop"
function Info([string]$msg) { Write-Host "[INFO] $msg" } function Info([string]$msg) { Write-Host "[INFO] $msg" }
function Warn([string]$msg) { Write-Host "[WARN] $msg" } function Warn([string]$msg) { Write-Host "[WARN] $msg" }
# Form data-binding tags (value = attribute path). A binding survives only if its root
# attribute is borrowed into the form's <Attributes>; otherwise it must be stripped or the
# platform rejects the form with "Неверный путь к данным" on load.
$script:formBindingDataTags = @('DataPath','TitleDataPath','FooterDataPath','HeaderDataPath','MultipleValueDataPath','MultipleValuePresentDataPath')
# Picture-path binding tags (value = picture index path, never a data attribute) — always stripped in the skeleton.
$script:formBindingPictureTags = @('RowPictureDataPath','MultipleValuePictureDataPath')
# Strip data-binding tags whose root attribute isn't borrowed.
# $keepObjekt=$true (BorrowMainAttribute): keep Объект.* data bindings, strip the rest.
# $keepObjekt=$false (default skeleton): strip all bindings. Picture-path tags are always stripped.
function Strip-FormBindings {
param([string]$xml, [bool]$keepObjekt)
foreach ($tag in $script:formBindingDataTags) {
if ($keepObjekt) {
$xml = [regex]::Replace($xml, "\s*<$tag>(?!Объект\.)[^<]*</$tag>", '')
} else {
$xml = [regex]::Replace($xml, "\s*<$tag>[^<]*</$tag>", '')
}
}
foreach ($tag in $script:formBindingPictureTags) {
$xml = [regex]::Replace($xml, "\s*<$tag>[^<]*</$tag>", '')
}
return $xml
}
# --- 1. Resolve paths --- # --- 1. Resolve paths ---
if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) { if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) {
$ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath $ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath
@@ -419,6 +444,14 @@ function Read-SourceObject {
$srcProps[$propName] = $propNode.InnerText.Trim() $srcProps[$propName] = $propNode.InnerText.Trim()
} }
} }
# DefinedType: carry the <Type> definition. A type alias is meaningless as a bare shell —
# the platform needs its underlying type (e.g. to know a column is a summable Number for totals).
if ($typeName -eq "DefinedType") {
$typeNode = $propsNode.SelectSingleNode("md:Type", $srcNs)
if ($typeNode) {
$srcProps["__TypeXml"] = [regex]::Replace($typeNode.OuterXml, '\s+xmlns(?::\w+)?="[^"]*"', '')
}
}
} }
return @{ return @{
@@ -481,8 +514,23 @@ function Borrow-Form {
} }
$srcFormContent = [System.IO.File]::ReadAllText($srcFormXmlPath, $enc) $srcFormContent = [System.IO.File]::ReadAllText($srcFormXmlPath, $enc)
# 3. Generate form metadata XML (ФормаЭлемента.xml) # 3. Generate form metadata XML (ФормаЭлемента.xml).
$newFormUuid = [guid]::NewGuid().ToString() # If the wrapper was already borrowed, reuse its uuid so re-borrow is idempotent
# (regenerating it would churn the form's identity on every rerun).
$formMetaFileExisting = Join-Path (Join-Path (Join-Path (Join-Path $extDir $dirName) $objName) "Forms") "${formName}.xml"
$newFormUuid = ""
if (Test-Path $formMetaFileExisting) {
try {
$existingDoc = New-Object System.Xml.XmlDocument
$existingDoc.Load($formMetaFileExisting)
$existingFormNode = $existingDoc.DocumentElement.SelectSingleNode("*[local-name()='Form']")
if ($existingFormNode) {
$existingUuid = $existingFormNode.GetAttribute("uuid")
if ($existingUuid) { $newFormUuid = $existingUuid }
}
} catch { }
}
if (-not $newFormUuid) { $newFormUuid = [guid]::NewGuid().ToString() }
$formMetaSb = New-Object System.Text.StringBuilder $formMetaSb = New-Object System.Text.StringBuilder
$formMetaSb.AppendLine("<?xml version=`"1.0`" encoding=`"UTF-8`"?>") | Out-Null $formMetaSb.AppendLine("<?xml version=`"1.0`" encoding=`"UTF-8`"?>") | Out-Null
$formMetaSb.AppendLine("<MetaDataObject $($script:xmlnsDecl) version=`"$($script:formatVersion)`">") | Out-Null $formMetaSb.AppendLine("<MetaDataObject $($script:xmlnsDecl) version=`"$($script:formatVersion)`">") | Out-Null
@@ -516,8 +564,10 @@ function Borrow-Form {
$srcFormDoc.Load($srcFormXmlPath) $srcFormDoc.Load($srcFormXmlPath)
$srcFormEl = $srcFormDoc.DocumentElement $srcFormEl = $srcFormDoc.DocumentElement
$formVersion = $srcFormEl.GetAttribute("version") # Borrowed form must use the extension's format version (not the source form's), so the whole
if (-not $formVersion) { $formVersion = $script:formatVersion } # extension stays uniform — otherwise the platform rejects the import on a version mismatch
# (e.g. a 2.13 form inside a 2.17 extension). The platform itself upgrades the form to the root version.
$formVersion = $script:formatVersion
# Find direct children: form properties, AutoCommandBar, ChildItems # Find direct children: form properties, AutoCommandBar, ChildItems
$srcAutoCmd = $null $srcAutoCmd = $null
@@ -552,13 +602,8 @@ function Borrow-Form {
$autoCmdXml = $autoCmdXml -replace '<Autofill>true</Autofill>', '<Autofill>false</Autofill>' $autoCmdXml = $autoCmdXml -replace '<Autofill>true</Autofill>', '<Autofill>false</Autofill>'
# Strip ExcludedCommand (references to standard commands invalid in extension) # Strip ExcludedCommand (references to standard commands invalid in extension)
$autoCmdXml = [regex]::Replace($autoCmdXml, '\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '') $autoCmdXml = [regex]::Replace($autoCmdXml, '\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '')
# Strip DataPath in AutoCommandBar buttons # Strip data-binding tags whose root attribute isn't borrowed
if ($BorrowMainAttr) { $autoCmdXml = Strip-FormBindings $autoCmdXml ([bool]$BorrowMainAttr)
# Keep only Объект.* DataPaths
$autoCmdXml = [regex]::Replace($autoCmdXml, '\s*<DataPath>(?!Объект\.)[^<]*</DataPath>', '')
} else {
$autoCmdXml = [regex]::Replace($autoCmdXml, '\s*<DataPath>[^<]*</DataPath>', '')
}
} }
# ChildItems: copy full tree, clean up base-config references # ChildItems: copy full tree, clean up base-config references
@@ -568,17 +613,9 @@ function Borrow-Form {
$childItemsXml = [regex]::Replace($childItemsXml, $nsStripPattern, '') $childItemsXml = [regex]::Replace($childItemsXml, $nsStripPattern, '')
# Replace all CommandName values with 0 # Replace all CommandName values with 0
$childItemsXml = [regex]::Replace($childItemsXml, '<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>') $childItemsXml = [regex]::Replace($childItemsXml, '<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>')
# Strip DataPath, TitleDataPath, RowPictureDataPath # Strip data-binding tags whose root attribute isn't borrowed
if ($BorrowMainAttr) { # (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*/RowPicture*)
# Keep only Объект.* DataPaths — strip form-attribute DataPaths (not borrowed) $childItemsXml = Strip-FormBindings $childItemsXml ([bool]$BorrowMainAttr)
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<DataPath>(?!Объект\.)[^<]*</DataPath>', '')
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<TitleDataPath>(?!Объект\.)[^<]*</TitleDataPath>', '')
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<RowPictureDataPath>[^<]*</RowPictureDataPath>', '')
} else {
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<DataPath>[^<]*</DataPath>', '')
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<TitleDataPath>[^<]*</TitleDataPath>', '')
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<RowPictureDataPath>[^<]*</RowPictureDataPath>', '')
}
# Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension) # Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension)
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '') $childItemsXml = [regex]::Replace($childItemsXml, '\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '')
# Strip TypeLink blocks with human-readable DataPath (Items.XXX — can't convert to UUID) # Strip TypeLink blocks with human-readable DataPath (Items.XXX — can't convert to UUID)
@@ -855,14 +892,19 @@ function Borrow-Form {
[System.IO.File]::WriteAllText($formXmlFile, $formXmlSb.ToString(), $enc) [System.IO.File]::WriteAllText($formXmlFile, $formXmlSb.ToString(), $enc)
Info " Created: $formXmlFile" Info " Created: $formXmlFile"
# 6. Create empty Module.bsl # 6. Create empty Module.bsl — but NEVER overwrite an existing one (re-borrow must
# not clobber user code added to the form module).
$moduleDir = Join-Path $formXmlDir "Form" $moduleDir = Join-Path $formXmlDir "Form"
if (-not (Test-Path $moduleDir)) { if (-not (Test-Path $moduleDir)) {
New-Item -ItemType Directory -Path $moduleDir -Force | Out-Null New-Item -ItemType Directory -Path $moduleDir -Force | Out-Null
} }
$moduleBslFile = Join-Path $moduleDir "Module.bsl" $moduleBslFile = Join-Path $moduleDir "Module.bsl"
[System.IO.File]::WriteAllText($moduleBslFile, "", $enc) if (Test-Path $moduleBslFile) {
Info " Created: $moduleBslFile" Info " Preserved existing Module.bsl"
} else {
[System.IO.File]::WriteAllText($moduleBslFile, "", $enc)
Info " Created: $moduleBslFile"
}
# 7. Register form in parent object ChildObjects # 7. Register form in parent object ChildObjects
Register-FormInObject $typeName $objName $formName Register-FormInObject $typeName $objName $formName
@@ -1010,8 +1052,29 @@ function Collect-FormDataPaths {
$firstLevel = @{} $firstLevel = @{}
$deepPaths = @() $deepPaths = @()
$matches2 = [regex]::Matches($content, '<DataPath>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</DataPath>') # Scan every data-binding tag (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*)
foreach ($m in $matches2) { # for Объект.* references — picture-path tags carry picture indices, not data attributes.
foreach ($tag in $script:formBindingDataTags) {
$bms = [regex]::Matches($content, "<$tag>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</$tag>")
foreach ($m in $bms) {
$path = $m.Groups[1].Value
$segments = $path.Split(".")
$seg0 = $segments[0]
if ($script:standardFields -contains $seg0) { continue }
$firstLevel[$seg0] = $true
if ($segments.Count -ge 2) {
$seg1 = $segments[1]
if ($script:standardFields -contains $seg1) { continue }
$seg2 = if ($segments.Count -ge 3) { $segments[2] } else { $null }
$deepPaths += @{ ObjectAttr = $seg0; SubAttr = $seg1; SubSubAttr = $seg2 }
}
}
}
# Also scan <Field>Объект.X</Field> — object attributes referenced by filter/conditional-appearance
# fields (and dynamic lists), not via a *DataPath binding (e.g. УдалитьЮрФизЛицо). Designer borrows these too.
$fieldMatches = [regex]::Matches($content, "<Field>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</Field>")
foreach ($m in $fieldMatches) {
$path = $m.Groups[1].Value $path = $m.Groups[1].Value
$segments = $path.Split(".") $segments = $path.Split(".")
$seg0 = $segments[0] $seg0 = $segments[0]
@@ -1024,21 +1087,11 @@ function Collect-FormDataPaths {
} }
} }
# Also collect from TitleDataPath
$matches3 = [regex]::Matches($content, '<TitleDataPath>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</TitleDataPath>')
foreach ($m in $matches3) {
$path = $m.Groups[1].Value
$segments = $path.Split(".")
$seg0 = $segments[0]
if ($script:standardFields -contains $seg0) { continue }
$firstLevel[$seg0] = $true
}
# Deduplicate deep paths # Deduplicate deep paths
$seen = @{} $seen = @{}
$uniqueDeep = @() $uniqueDeep = @()
foreach ($dp in $deepPaths) { foreach ($dp in $deepPaths) {
$key = "$($dp.ObjectAttr).$($dp.SubAttr)" $key = "$($dp.ObjectAttr).$($dp.SubAttr).$($dp.SubSubAttr)"
if (-not $seen.ContainsKey($key)) { if (-not $seen.ContainsKey($key)) {
$seen[$key] = $true $seen[$key] = $true
$uniqueDeep += $dp $uniqueDeep += $dp
@@ -1142,7 +1195,8 @@ function Resolve-SourceAttributes {
} }
# Extract extra Properties for main object enrichment (Hierarchical, CodeLength, etc.) # Extract extra Properties for main object enrichment (Hierarchical, CodeLength, etc.)
$extraProps = @{} # Ordered so PS emits the same property order as the Python port (dict preserves insertion order).
$extraProps = [ordered]@{}
$propsNode = $srcEl.SelectSingleNode("md:Properties", $srcNs) $propsNode = $srcEl.SelectSingleNode("md:Properties", $srcNs)
if ($propsNode) { if ($propsNode) {
$propsToExtract = @("Hierarchical","FoldersOnTop","CodeLength","DescriptionLength","CodeType","CodeAllowedLength", $propsToExtract = @("Hierarchical","FoldersOnTop","CodeLength","DescriptionLength","CodeType","CodeAllowedLength",
@@ -1375,28 +1429,45 @@ function Borrow-MainAttribute {
# Step 3: Build the adopted content and insert into main object XML # Step 3: Build the adopted content and insert into main object XML
$objFile = Join-Path (Join-Path $extDir $dirName) "${objName}.xml" $objFile = Join-Path (Join-Path $extDir $dirName) "${objName}.xml"
# Read existing object XML (needed for dedup + enrichment)
$objContent = [System.IO.File]::ReadAllText($objFile, (New-Object System.Text.UTF8Encoding($true)))
# Dedup: skip attributes/TS already present in object's ChildObjects (idempotent re-borrow)
$existingChildNames = @{}
if ($objContent -match '(?s)<ChildObjects>(.*?)</ChildObjects>') {
foreach ($nm in [regex]::Matches($Matches[1], '<Name>(\w+)</Name>')) {
$existingChildNames[$nm.Groups[1].Value] = $true
}
}
$insertAttrs = @($srcAttrs | Where-Object { -not $existingChildNames.ContainsKey($_.Name) })
$insertTS = @($srcTS | Where-Object { -not $existingChildNames.ContainsKey($_.Name) })
# Generate full object XML with attributes and TS # Generate full object XML with attributes and TS
$contentSb = New-Object System.Text.StringBuilder $contentSb = New-Object System.Text.StringBuilder
foreach ($attr in $srcAttrs) { foreach ($attr in $insertAttrs) {
$attrXml = Build-AdoptedAttributeXml $attr.Name $attr.Uuid $attr.TypeXml "`t`t`t" $attrXml = Build-AdoptedAttributeXml $attr.Name $attr.Uuid $attr.TypeXml "`t`t`t"
$contentSb.AppendLine($attrXml) | Out-Null $contentSb.AppendLine($attrXml) | Out-Null
} }
foreach ($ts in $srcTS) { foreach ($ts in $insertTS) {
$tsXml = Build-AdoptedTabularSectionXml $ts.Name $ts.Uuid $ts.GeneratedTypes $ts.Attributes "`t`t`t" $tsXml = Build-AdoptedTabularSectionXml $ts.Name $ts.Uuid $ts.GeneratedTypes $ts.Attributes "`t`t`t"
$contentSb.AppendLine($tsXml) | Out-Null $contentSb.AppendLine($tsXml) | Out-Null
} }
$adoptedContent = $contentSb.ToString().TrimEnd() $adoptedContent = $contentSb.ToString().TrimEnd()
# Read existing object XML and inject # Inject extra properties into the object's OWN Properties only — idempotent and anchored to the
$objContent = [System.IO.File]::ReadAllText($objFile, (New-Object System.Text.UTF8Encoding($true))) # first ExtendedConfigurationObject (the object's). On re-borrow, adopted attributes each have their
# own ExtendedConfigurationObject; a global replace would push object props inside every <Attribute>.
# Inject extra properties after ExtendedConfigurationObject
if ($extraProps.Count -gt 0) { if ($extraProps.Count -gt 0) {
$objPropsBlock = ""
if ($objContent -match '(?s)<Properties>(.*?)</Properties>') { $objPropsBlock = $Matches[1] }
$propsSb = New-Object System.Text.StringBuilder $propsSb = New-Object System.Text.StringBuilder
foreach ($pName in $extraProps.Keys) { foreach ($pName in $extraProps.Keys) {
if ($objPropsBlock -match "<$pName>") { continue }
$propsSb.Append("`r`n`t`t`t<${pName}>$($extraProps[$pName])</${pName}>") | Out-Null $propsSb.Append("`r`n`t`t`t<${pName}>$($extraProps[$pName])</${pName}>") | Out-Null
} }
$objContent = $objContent -replace '(</ExtendedConfigurationObject>)', "`$1$($propsSb.ToString())" if ($propsSb.Length -gt 0) {
$objContent = ([regex]'</ExtendedConfigurationObject>').Replace($objContent, "</ExtendedConfigurationObject>$($propsSb.ToString())", 1)
}
} }
# Replace empty ChildObjects with adopted content # Replace empty ChildObjects with adopted content
@@ -1454,79 +1525,46 @@ function Borrow-MainAttribute {
# Step 5: Handle deep paths (Form mode only) # Step 5: Handle deep paths (Form mode only)
if ($mode -eq "Form" -and $deepPaths.Count -gt 0) { if ($mode -eq "Form" -and $deepPaths.Count -gt 0) {
# Filter out deep paths where ObjectAttr is a TabularSection (those are TS column refs, not deep attribute refs) # Top-level ref deep paths: Объект.<Ref>.<Sub> — borrow the ref attribute's catalog with the sub-attribute
$realDeep = @() $deepByAttr = @{}
foreach ($dp in $deepPaths) { foreach ($dp in $deepPaths) {
if (-not $tsNames.ContainsKey($dp.ObjectAttr)) { $realDeep += $dp } if ($tsNames.ContainsKey($dp.ObjectAttr)) { continue }
if (-not $deepByAttr.ContainsKey($dp.ObjectAttr)) { $deepByAttr[$dp.ObjectAttr] = @() }
if ($deepByAttr[$dp.ObjectAttr] -notcontains $dp.SubAttr) { $deepByAttr[$dp.ObjectAttr] += $dp.SubAttr }
} }
if ($deepByAttr.Count -gt 0) {
if ($realDeep.Count -gt 0) { Info " Processing $($deepByAttr.Count) deep path attribute(s)..."
Info " Processing $($realDeep.Count) deep path(s)..."
# Group by ObjectAttr → target catalog
$deepByAttr = @{}
foreach ($dp in $realDeep) {
if (-not $deepByAttr.ContainsKey($dp.ObjectAttr)) { $deepByAttr[$dp.ObjectAttr] = @() }
$deepByAttr[$dp.ObjectAttr] += $dp.SubAttr
}
foreach ($attrName in $deepByAttr.Keys) { foreach ($attrName in $deepByAttr.Keys) {
# Find the attribute's type to determine target catalog
$attrInfo = $srcAttrs | Where-Object { $_.Name -eq $attrName } | Select-Object -First 1 $attrInfo = $srcAttrs | Where-Object { $_.Name -eq $attrName } | Select-Object -First 1
if (-not $attrInfo) { continue } if (-not $attrInfo) { continue }
# Extract catalog name from type: cfg:CatalogRef.XXX
$catMatch = [regex]::Match($attrInfo.TypeXml, 'cfg:(\w+)Ref\.(\w+)') $catMatch = [regex]::Match($attrInfo.TypeXml, 'cfg:(\w+)Ref\.(\w+)')
if (-not $catMatch.Success) { continue } if (-not $catMatch.Success) { continue }
Borrow-DeepTargetAttrs $catMatch.Groups[1].Value $catMatch.Groups[2].Value $deepByAttr[$attrName]
}
}
$targetTypeName = $catMatch.Groups[1].Value # Tabular-section deep paths: Объект.<ТЧ>.<Колонка>.<Sub> — borrow the column's catalog with the sub-attribute
$targetObjName = $catMatch.Groups[2].Value $tsDeepByCol = @{}
foreach ($dp in $deepPaths) {
# Ensure target is borrowed if (-not $tsNames.ContainsKey($dp.ObjectAttr)) { continue }
if (-not (Test-ObjectBorrowed $targetTypeName $targetObjName)) { if (-not $dp.SubSubAttr) { continue }
$tSrc = Read-SourceObject $targetTypeName $targetObjName if ($script:standardFields -contains $dp.SubSubAttr) { continue }
$tBorrowedXml = Build-BorrowedObjectXml $targetTypeName $targetObjName $tSrc.Uuid $tSrc.Properties $k = "$($dp.ObjectAttr)|$($dp.SubAttr)"
$tTargetDir = Join-Path $extDir $childTypeDirMap[$targetTypeName] if (-not $tsDeepByCol.ContainsKey($k)) { $tsDeepByCol[$k] = @() }
if (-not (Test-Path $tTargetDir)) { if ($tsDeepByCol[$k] -notcontains $dp.SubSubAttr) { $tsDeepByCol[$k] += $dp.SubSubAttr }
New-Item -ItemType Directory -Path $tTargetDir -Force | Out-Null }
} if ($tsDeepByCol.Count -gt 0) {
$tTargetFile = Join-Path $tTargetDir "${targetObjName}.xml" Info " Processing $($tsDeepByCol.Count) tabular-section deep path(s)..."
[System.IO.File]::WriteAllText($tTargetFile, $tBorrowedXml, $encBom) foreach ($k in $tsDeepByCol.Keys) {
Add-ToChildObjects $targetTypeName $targetObjName $parts = $k.Split("|")
$script:borrowedFiles += $tTargetFile $tsName = $parts[0]; $colName = $parts[1]
Info " Auto-borrowed for deep path: ${targetTypeName}.${targetObjName}" $tsInfo = $srcTS | Where-Object { $_.Name -eq $tsName } | Select-Object -First 1
} if (-not $tsInfo) { continue }
$colInfo = $tsInfo.Attributes | Where-Object { $_.Name -eq $colName } | Select-Object -First 1
# Resolve sub-attributes in target catalog if (-not $colInfo) { continue }
$subNames = @{} $catMatch = [regex]::Match($colInfo.TypeXml, 'cfg:(\w+)Ref\.(\w+)')
foreach ($sn in $deepByAttr[$attrName]) { $subNames[$sn] = $true } if (-not $catMatch.Success) { continue }
$subResolved = Resolve-SourceAttributes $targetTypeName $targetObjName $subNames Borrow-DeepTargetAttrs $catMatch.Groups[1].Value $catMatch.Groups[2].Value $tsDeepByCol[$k]
if ($subResolved.Attributes.Count -gt 0) {
Merge-AttributesIntoObject $targetTypeName $targetObjName $subResolved.Attributes
# Collect and borrow ref types from deep attributes
$subTypeXmls = @()
foreach ($sa in $subResolved.Attributes) { $subTypeXmls += $sa.TypeXml }
$subRefTypes = Collect-ReferenceTypes $subTypeXmls
foreach ($srt in $subRefTypes) {
if (-not $childTypeDirMap.ContainsKey($srt.TypeName)) { continue }
if (Test-ObjectBorrowed $srt.TypeName $srt.ObjName) { continue }
$sSrcFile = Join-Path (Join-Path $cfgDir $childTypeDirMap[$srt.TypeName]) "$($srt.ObjName).xml"
if (-not (Test-Path $sSrcFile)) { continue }
$sSrc = Read-SourceObject $srt.TypeName $srt.ObjName
$sBorrowedXml = Build-BorrowedObjectXml $srt.TypeName $srt.ObjName $sSrc.Uuid $sSrc.Properties
$sTargetDir = Join-Path $extDir $childTypeDirMap[$srt.TypeName]
if (-not (Test-Path $sTargetDir)) {
New-Item -ItemType Directory -Path $sTargetDir -Force | Out-Null
}
$sTargetFile = Join-Path $sTargetDir "$($srt.ObjName).xml"
[System.IO.File]::WriteAllText($sTargetFile, $sBorrowedXml, $encBom)
Add-ToChildObjects $srt.TypeName $srt.ObjName
$script:borrowedFiles += $sTargetFile
Info " Auto-borrowed (deep): $($srt.TypeName).$($srt.ObjName)"
}
}
} }
} }
} }
@@ -1534,6 +1572,57 @@ function Borrow-MainAttribute {
Info " Main attribute borrowing complete" Info " Main attribute borrowing complete"
} }
# --- 11i. Helper: borrow a deep-path target catalog together with the referenced sub-attributes ---
# Used for both Объект.<Ref>.<Sub> (top-level ref attr) and Объект.<ТЧ>.<Колонка>.<Sub> (tabular-section
# column ref). Mirrors Designer: the referenced catalog is adopted WITH the sub-attributes the form shows,
# otherwise the platform rejects the deep DataPath ("Неверный путь к данным").
function Borrow-DeepTargetAttrs {
param([string]$targetTypeName, [string]$targetObjName, $subAttrNames)
$encBomLocal = New-Object System.Text.UTF8Encoding($true)
# Ensure target is borrowed (shell)
if (-not (Test-ObjectBorrowed $targetTypeName $targetObjName)) {
$tSrc = Read-SourceObject $targetTypeName $targetObjName
$tBorrowedXml = Build-BorrowedObjectXml $targetTypeName $targetObjName $tSrc.Uuid $tSrc.Properties
$tTargetDir = Join-Path $extDir $childTypeDirMap[$targetTypeName]
if (-not (Test-Path $tTargetDir)) { New-Item -ItemType Directory -Path $tTargetDir -Force | Out-Null }
$tTargetFile = Join-Path $tTargetDir "${targetObjName}.xml"
[System.IO.File]::WriteAllText($tTargetFile, $tBorrowedXml, $encBomLocal)
Add-ToChildObjects $targetTypeName $targetObjName
$script:borrowedFiles += $tTargetFile
Info " Auto-borrowed for deep path: ${targetTypeName}.${targetObjName}"
}
# Resolve sub-attributes in target catalog and merge them in
$subNames = @{}
foreach ($sn in $subAttrNames) { $subNames[$sn] = $true }
$subResolved = Resolve-SourceAttributes $targetTypeName $targetObjName $subNames
if ($subResolved.Attributes.Count -gt 0) {
Merge-AttributesIntoObject $targetTypeName $targetObjName $subResolved.Attributes
# Borrow ref types referenced by the sub-attributes
$subTypeXmls = @()
foreach ($sa in $subResolved.Attributes) { $subTypeXmls += $sa.TypeXml }
$subRefTypes = Collect-ReferenceTypes $subTypeXmls
foreach ($srt in $subRefTypes) {
if (-not $childTypeDirMap.ContainsKey($srt.TypeName)) { continue }
if (Test-ObjectBorrowed $srt.TypeName $srt.ObjName) { continue }
$sSrcFile = Join-Path (Join-Path $cfgDir $childTypeDirMap[$srt.TypeName]) "$($srt.ObjName).xml"
if (-not (Test-Path $sSrcFile)) { continue }
$sSrc = Read-SourceObject $srt.TypeName $srt.ObjName
$sBorrowedXml = Build-BorrowedObjectXml $srt.TypeName $srt.ObjName $sSrc.Uuid $sSrc.Properties
$sTargetDir = Join-Path $extDir $childTypeDirMap[$srt.TypeName]
if (-not (Test-Path $sTargetDir)) { New-Item -ItemType Directory -Path $sTargetDir -Force | Out-Null }
$sTargetFile = Join-Path $sTargetDir "$($srt.ObjName).xml"
[System.IO.File]::WriteAllText($sTargetFile, $sBorrowedXml, $encBomLocal)
Add-ToChildObjects $srt.TypeName $srt.ObjName
$script:borrowedFiles += $sTargetFile
Info " Auto-borrowed (deep): $($srt.TypeName).$($srt.ObjName)"
}
}
}
# --- 12. Helper: build borrowed object XML --- # --- 12. Helper: build borrowed object XML ---
function Build-BorrowedObjectXml { function Build-BorrowedObjectXml {
param( param(
@@ -1572,6 +1661,11 @@ function Build-BorrowedObjectXml {
} }
} }
# DefinedType: emit the carried <Type> definition (needed for the alias to resolve, e.g. totals)
if ($typeName -eq "DefinedType" -and $sourceProps.ContainsKey("__TypeXml")) {
$sb.AppendLine("`t`t`t$($sourceProps['__TypeXml'])") | Out-Null
}
$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)
+202 -113
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# cfe-borrow v1.3 — Borrow objects from configuration into extension (CFE) # cfe-borrow v1.8 — 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
@@ -14,6 +14,36 @@ XR_NS = "http://v8.1c.ru/8.3/xcf/readable"
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance" XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
V8_NS = "http://v8.1c.ru/8.1/data/core" V8_NS = "http://v8.1c.ru/8.1/data/core"
# Form data-binding tags (value = attribute path). A binding survives only if its root
# attribute is borrowed into the form's <Attributes>; otherwise it must be stripped or the
# platform rejects the form with "Неверный путь к данным" on load.
FORM_BINDING_DATA_TAGS = ["DataPath", "TitleDataPath", "FooterDataPath", "HeaderDataPath", "MultipleValueDataPath", "MultipleValuePresentDataPath"]
# Picture-path binding tags (value = picture index path, never a data attribute) — always stripped in the skeleton.
FORM_BINDING_PICTURE_TAGS = ["RowPictureDataPath", "MultipleValuePictureDataPath"]
def strip_form_bindings(xml, keep_objekt):
"""Strip data-binding tags whose root attribute isn't borrowed.
keep_objekt=True (BorrowMainAttribute): keep Объект.* data bindings, strip the rest.
keep_objekt=False (default skeleton): strip all bindings. Picture-path tags are always stripped."""
for tag in FORM_BINDING_DATA_TAGS:
if keep_objekt:
xml = re.sub(rf'\s*<{tag}>(?!Объект\.)[^<]*</{tag}>', '', xml)
else:
xml = re.sub(rf'\s*<{tag}>[^<]*</{tag}>', '', xml)
for tag in FORM_BINDING_PICTURE_TAGS:
xml = re.sub(rf'\s*<{tag}>[^<]*</{tag}>', '', xml)
return xml
def decode_numeric_entities(s):
"""lxml emits numeric character refs (&#xNNNN;) for non-ASCII in some self-closed
elements where the PowerShell port writes literal characters. Normalize numeric refs
back to literal so PS↔PY output matches. Named entities (&amp; &lt; ...) are left intact."""
s = re.sub(r'&#x([0-9A-Fa-f]+);', lambda m: chr(int(m.group(1), 16)), s)
s = re.sub(r'&#(\d+);', lambda m: chr(int(m.group(1))), s)
return s
def localname(el): def localname(el):
return etree.QName(el.tag).localname return etree.QName(el.tag).localname
@@ -462,6 +492,13 @@ def main():
prop_node = props_node.find(f"{{{MD_NS}}}{prop_name}") prop_node = props_node.find(f"{{{MD_NS}}}{prop_name}")
if prop_node is not None: if prop_node is not None:
src_props[prop_name] = (prop_node.text or "").strip() src_props[prop_name] = (prop_node.text or "").strip()
# DefinedType: carry the <Type> definition. A type alias is meaningless as a bare shell —
# the platform needs its underlying type (e.g. to know a column is a summable Number for totals).
if type_name == "DefinedType":
type_node = props_node.find(f"{{{MD_NS}}}Type")
if type_node is not None:
type_xml = etree.tostring(type_node, encoding="unicode")
src_props["__TypeXml"] = re.sub(r'\s+xmlns(?::\w+)?="[^"]*"', '', type_xml)
return {"Uuid": src_uuid, "Properties": src_props, "Element": src_el} return {"Uuid": src_uuid, "Properties": src_props, "Element": src_el}
@@ -533,6 +570,10 @@ def main():
prop_val = source_props.get(prop_name, "false") prop_val = source_props.get(prop_name, "false")
lines.append(f"\t\t\t<{prop_name}>{prop_val}</{prop_name}>") lines.append(f"\t\t\t<{prop_name}>{prop_val}</{prop_name}>")
# DefinedType: emit the carried <Type> definition (needed for the alias to resolve, e.g. totals)
if type_name == "DefinedType" and "__TypeXml" in source_props:
lines.append(f"\t\t\t{source_props['__TypeXml']}")
lines.append("\t\t</Properties>") lines.append("\t\t</Properties>")
if type_name in TYPES_WITH_CHILD_OBJECTS: if type_name in TYPES_WITH_CHILD_OBJECTS:
@@ -644,7 +685,26 @@ def main():
first_level = {} first_level = {}
deep_paths = [] deep_paths = []
for m in re.finditer(r'<DataPath>[^<]*\b\u041e\u0431\u044a\u0435\u043a\u0442\.(\w+(?:\.\w+)*)</DataPath>', content): # Scan every data-binding tag (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*)
# for Объект.* references — picture-path tags carry picture indices, not data attributes.
for tag in FORM_BINDING_DATA_TAGS:
for m in re.finditer(r'<' + tag + r'>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</' + tag + r'>', content):
path = m.group(1)
segments = path.split(".")
seg0 = segments[0]
if seg0 in STANDARD_FIELDS:
continue
first_level[seg0] = True
if len(segments) >= 2:
seg1 = segments[1]
if seg1 in STANDARD_FIELDS:
continue
seg2 = segments[2] if len(segments) >= 3 else None
deep_paths.append({"ObjectAttr": seg0, "SubAttr": seg1, "SubSubAttr": seg2})
# Also scan <Field>Объект.X</Field> — object attributes referenced by filter/conditional-appearance
# fields (and dynamic lists), not via a *DataPath binding (e.g. УдалитьЮрФизЛицо). Designer borrows these too.
for m in re.finditer(r'<Field>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</Field>', content):
path = m.group(1) path = m.group(1)
segments = path.split(".") segments = path.split(".")
seg0 = segments[0] seg0 = segments[0]
@@ -655,22 +715,14 @@ def main():
seg1 = segments[1] seg1 = segments[1]
if seg1 in STANDARD_FIELDS: if seg1 in STANDARD_FIELDS:
continue continue
deep_paths.append({"ObjectAttr": seg0, "SubAttr": seg1}) seg2 = segments[2] if len(segments) >= 3 else None
deep_paths.append({"ObjectAttr": seg0, "SubAttr": seg1, "SubSubAttr": seg2})
# Also collect from TitleDataPath
for m in re.finditer(r'<TitleDataPath>[^<]*\b\u041e\u0431\u044a\u0435\u043a\u0442\.(\w+(?:\.\w+)*)</TitleDataPath>', content):
path = m.group(1)
segments = path.split(".")
seg0 = segments[0]
if seg0 in STANDARD_FIELDS:
continue
first_level[seg0] = True
# Deduplicate deep paths # Deduplicate deep paths
seen = set() seen = set()
unique_deep = [] unique_deep = []
for dp in deep_paths: for dp in deep_paths:
key = f"{dp['ObjectAttr']}.{dp['SubAttr']}" key = f"{dp['ObjectAttr']}.{dp['SubAttr']}.{dp.get('SubSubAttr')}"
if key not in seen: if key not in seen:
seen.add(key) seen.add(key)
unique_deep.append(dp) unique_deep.append(dp)
@@ -941,26 +993,40 @@ def main():
# Step 3: Build the adopted content and insert into main object XML # Step 3: Build the adopted content and insert into main object XML
obj_file = os.path.join(ext_dir, dir_name, f"{obj_name}.xml") obj_file = os.path.join(ext_dir, dir_name, f"{obj_name}.xml")
# Generate full object XML with attributes and TS # Read existing object XML (needed for dedup + enrichment)
content_parts = []
for attr in src_attrs:
attr_xml = build_adopted_attribute_xml(attr["Name"], attr["Uuid"], attr["TypeXml"], "\t\t\t")
content_parts.append(attr_xml)
for ts in src_ts:
ts_xml = build_adopted_tabular_section_xml(ts["Name"], ts["Uuid"], ts["GeneratedTypes"], ts["Attributes"], "\t\t\t")
content_parts.append(ts_xml)
adopted_content = "\n".join(content_parts).rstrip()
# Read existing object XML and inject
with open(obj_file, "r", encoding="utf-8-sig") as fh: with open(obj_file, "r", encoding="utf-8-sig") as fh:
obj_content = fh.read() obj_content = fh.read()
# Inject extra properties after ExtendedConfigurationObject # Dedup: skip attributes/TS already present in object's ChildObjects (idempotent re-borrow)
existing_child_names = set()
m_co = re.search(r'(?s)<ChildObjects>(.*?)</ChildObjects>', obj_content)
if m_co:
for nm in re.findall(r'<Name>(\w+)</Name>', m_co.group(1)):
existing_child_names.add(nm)
insert_attrs = [a for a in src_attrs if a["Name"] not in existing_child_names]
insert_ts = [t for t in src_ts if t["Name"] not in existing_child_names]
# Generate full object XML with attributes and TS
content_parts = []
for attr in insert_attrs:
content_parts.append(build_adopted_attribute_xml(attr["Name"], attr["Uuid"], attr["TypeXml"], "\t\t\t"))
for ts in insert_ts:
content_parts.append(build_adopted_tabular_section_xml(ts["Name"], ts["Uuid"], ts["GeneratedTypes"], ts["Attributes"], "\t\t\t"))
adopted_content = "\n".join(content_parts).rstrip()
# Inject extra properties into the object's OWN Properties only — idempotent and anchored to the
# first ExtendedConfigurationObject (the object's). On re-borrow, adopted attributes each have their
# own ExtendedConfigurationObject; a global replace would push object props inside every <Attribute>.
if extra_props: if extra_props:
m_props = re.search(r'(?s)<Properties>(.*?)</Properties>', obj_content)
obj_props_block = m_props.group(1) if m_props else ""
props_xml = "" props_xml = ""
for p_name, p_val in extra_props.items(): for p_name, p_val in extra_props.items():
if f"<{p_name}>" in obj_props_block:
continue
props_xml += f"\r\n\t\t\t<{p_name}>{p_val}</{p_name}>" props_xml += f"\r\n\t\t\t<{p_name}>{p_val}</{p_name}>"
obj_content = obj_content.replace("</ExtendedConfigurationObject>", f"</ExtendedConfigurationObject>{props_xml}") if props_xml:
obj_content = obj_content.replace("</ExtendedConfigurationObject>", f"</ExtendedConfigurationObject>{props_xml}", 1)
# Replace empty ChildObjects with adopted content # Replace empty ChildObjects with adopted content
if adopted_content: if adopted_content:
@@ -1012,79 +1078,93 @@ def main():
# Step 5: Handle deep paths (Form mode only) # Step 5: Handle deep paths (Form mode only)
if mode == "Form" and deep_paths: if mode == "Form" and deep_paths:
# Filter out deep paths where ObjectAttr is a TabularSection # Top-level ref deep paths: Объект.<Ref>.<Sub> — borrow the ref attribute's catalog with the sub-attribute
real_deep = [dp for dp in deep_paths if dp["ObjectAttr"] not in ts_names] deep_by_attr = {}
for dp in deep_paths:
if real_deep: if dp["ObjectAttr"] in ts_names:
info(f" Processing {len(real_deep)} deep path(s)...") continue
deep_by_attr.setdefault(dp["ObjectAttr"], [])
# Group by ObjectAttr -> target catalog if dp["SubAttr"] not in deep_by_attr[dp["ObjectAttr"]]:
deep_by_attr = {}
for dp in real_deep:
if dp["ObjectAttr"] not in deep_by_attr:
deep_by_attr[dp["ObjectAttr"]] = []
deep_by_attr[dp["ObjectAttr"]].append(dp["SubAttr"]) deep_by_attr[dp["ObjectAttr"]].append(dp["SubAttr"])
if deep_by_attr:
info(f" Processing {len(deep_by_attr)} deep path attribute(s)...")
for attr_name, sub_attr_names in deep_by_attr.items(): for attr_name, sub_attr_names in deep_by_attr.items():
# Find the attribute's type to determine target catalog attr_info = next((a for a in src_attrs if a["Name"] == attr_name), None)
attr_info = None
for a in src_attrs:
if a["Name"] == attr_name:
attr_info = a
break
if not attr_info: if not attr_info:
continue continue
# Extract catalog name from type: cfg:CatalogRef.XXX
cat_match = re.search(r'cfg:(\w+)Ref\.(\w+)', attr_info["TypeXml"]) cat_match = re.search(r'cfg:(\w+)Ref\.(\w+)', attr_info["TypeXml"])
if not cat_match: if not cat_match:
continue continue
borrow_deep_target_attrs(cat_match.group(1), cat_match.group(2), sub_attr_names)
target_type_name = cat_match.group(1) # Tabular-section deep paths: Объект.<ТЧ>.<Колонка>.<Sub> — borrow the column's catalog with the sub-attribute
target_obj_name = cat_match.group(2) ts_deep_by_col = {}
for dp in deep_paths:
# Ensure target is borrowed if dp["ObjectAttr"] not in ts_names:
if not test_object_borrowed(target_type_name, target_obj_name): continue
t_src = read_source_object(target_type_name, target_obj_name) if not dp.get("SubSubAttr"):
t_borrowed_xml = build_borrowed_object_xml(target_type_name, target_obj_name, t_src["Uuid"], t_src["Properties"]) continue
t_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[target_type_name]) if dp["SubSubAttr"] in STANDARD_FIELDS:
os.makedirs(t_target_dir, exist_ok=True) continue
t_target_file = os.path.join(t_target_dir, f"{target_obj_name}.xml") k = (dp["ObjectAttr"], dp["SubAttr"])
save_text_bom(t_target_file, t_borrowed_xml) ts_deep_by_col.setdefault(k, [])
add_to_child_objects(target_type_name, target_obj_name) if dp["SubSubAttr"] not in ts_deep_by_col[k]:
borrowed_files.append(t_target_file) ts_deep_by_col[k].append(dp["SubSubAttr"])
info(f" Auto-borrowed for deep path: {target_type_name}.{target_obj_name}") if ts_deep_by_col:
info(f" Processing {len(ts_deep_by_col)} tabular-section deep path(s)...")
# Resolve sub-attributes in target catalog for (ts_name, col_name), sub_attr_names in ts_deep_by_col.items():
sub_names = {sn: True for sn in sub_attr_names} ts_info = next((t for t in src_ts if t["Name"] == ts_name), None)
sub_resolved = resolve_source_attributes(target_type_name, target_obj_name, sub_names) if not ts_info:
continue
if sub_resolved["Attributes"]: col_info = next((c for c in ts_info["Attributes"] if c["Name"] == col_name), None)
merge_attributes_into_object(target_type_name, target_obj_name, sub_resolved["Attributes"]) if not col_info:
continue
# Collect and borrow ref types from deep attributes cat_match = re.search(r'cfg:(\w+)Ref\.(\w+)', col_info["TypeXml"])
sub_type_xmls = [sa["TypeXml"] for sa in sub_resolved["Attributes"]] if not cat_match:
sub_ref_types = collect_reference_types(sub_type_xmls) continue
for srt in sub_ref_types: borrow_deep_target_attrs(cat_match.group(1), cat_match.group(2), sub_attr_names)
if srt["TypeName"] not in CHILD_TYPE_DIR_MAP:
continue
if test_object_borrowed(srt["TypeName"], srt["ObjName"]):
continue
s_src_file = os.path.join(cfg_dir, CHILD_TYPE_DIR_MAP[srt["TypeName"]], f"{srt['ObjName']}.xml")
if not os.path.isfile(s_src_file):
continue
s_src = read_source_object(srt["TypeName"], srt["ObjName"])
s_borrowed_xml = build_borrowed_object_xml(srt["TypeName"], srt["ObjName"], s_src["Uuid"], s_src["Properties"])
s_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[srt["TypeName"]])
os.makedirs(s_target_dir, exist_ok=True)
s_target_file = os.path.join(s_target_dir, f"{srt['ObjName']}.xml")
save_text_bom(s_target_file, s_borrowed_xml)
add_to_child_objects(srt["TypeName"], srt["ObjName"])
borrowed_files.append(s_target_file)
info(f" Auto-borrowed (deep): {srt['TypeName']}.{srt['ObjName']}")
info(" Main attribute borrowing complete") info(" Main attribute borrowing complete")
def borrow_deep_target_attrs(target_type_name, target_obj_name, sub_attr_names):
# Borrow a deep-path target catalog together with the referenced sub-attributes, for both
# Объект.<Ref>.<Sub> and Объект.<ТЧ>.<Колонка>.<Sub>. Mirrors Designer: the referenced catalog
# is adopted WITH the sub-attributes the form shows, else the platform rejects the deep DataPath.
if not test_object_borrowed(target_type_name, target_obj_name):
t_src = read_source_object(target_type_name, target_obj_name)
t_borrowed_xml = build_borrowed_object_xml(target_type_name, target_obj_name, t_src["Uuid"], t_src["Properties"])
t_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[target_type_name])
os.makedirs(t_target_dir, exist_ok=True)
t_target_file = os.path.join(t_target_dir, f"{target_obj_name}.xml")
save_text_bom(t_target_file, t_borrowed_xml)
add_to_child_objects(target_type_name, target_obj_name)
borrowed_files.append(t_target_file)
info(f" Auto-borrowed for deep path: {target_type_name}.{target_obj_name}")
sub_names = {sn: True for sn in sub_attr_names}
sub_resolved = resolve_source_attributes(target_type_name, target_obj_name, sub_names)
if sub_resolved["Attributes"]:
merge_attributes_into_object(target_type_name, target_obj_name, sub_resolved["Attributes"])
sub_type_xmls = [sa["TypeXml"] for sa in sub_resolved["Attributes"]]
sub_ref_types = collect_reference_types(sub_type_xmls)
for srt in sub_ref_types:
if srt["TypeName"] not in CHILD_TYPE_DIR_MAP:
continue
if test_object_borrowed(srt["TypeName"], srt["ObjName"]):
continue
s_src_file = os.path.join(cfg_dir, CHILD_TYPE_DIR_MAP[srt["TypeName"]], f"{srt['ObjName']}.xml")
if not os.path.isfile(s_src_file):
continue
s_src = read_source_object(srt["TypeName"], srt["ObjName"])
s_borrowed_xml = build_borrowed_object_xml(srt["TypeName"], srt["ObjName"], s_src["Uuid"], s_src["Properties"])
s_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[srt["TypeName"]])
os.makedirs(s_target_dir, exist_ok=True)
s_target_file = os.path.join(s_target_dir, f"{srt['ObjName']}.xml")
save_text_bom(s_target_file, s_borrowed_xml)
add_to_child_objects(srt["TypeName"], srt["ObjName"])
borrowed_files.append(s_target_file)
info(f" Auto-borrowed (deep): {srt['TypeName']}.{srt['ObjName']}")
def borrow_form(type_name, obj_name, form_name, borrow_main_attr=False): def borrow_form(type_name, obj_name, form_name, borrow_main_attr=False):
dir_name = CHILD_TYPE_DIR_MAP[type_name] dir_name = CHILD_TYPE_DIR_MAP[type_name]
@@ -1100,8 +1180,22 @@ def main():
with open(src_form_xml_path, "r", encoding="utf-8-sig") as fh: with open(src_form_xml_path, "r", encoding="utf-8-sig") as fh:
src_form_content = fh.read() src_form_content = fh.read()
# 3. Generate form metadata XML # 3. Generate form metadata XML.
new_form_uuid = new_guid() # If the wrapper was already borrowed, reuse its uuid so re-borrow is idempotent
# (regenerating it would churn the form's identity on every rerun).
existing_wrapper = os.path.join(ext_dir, dir_name, obj_name, "Forms", f"{form_name}.xml")
new_form_uuid = ""
if os.path.isfile(existing_wrapper):
try:
existing_root = etree.parse(existing_wrapper).getroot()
for c in existing_root:
if isinstance(c.tag, str) and localname(c) == "Form":
new_form_uuid = c.get("uuid", "") or ""
break
except Exception:
new_form_uuid = ""
if not new_form_uuid:
new_form_uuid = new_guid()
form_meta_lines = [ form_meta_lines = [
'<?xml version="1.0" encoding="UTF-8"?>', '<?xml version="1.0" encoding="UTF-8"?>',
f'<MetaDataObject {XMLNS_DECL} version="{format_version}">', f'<MetaDataObject {XMLNS_DECL} version="{format_version}">',
@@ -1131,7 +1225,10 @@ def main():
src_form_tree = etree.parse(src_form_xml_path, src_form_parser) src_form_tree = etree.parse(src_form_xml_path, src_form_parser)
src_form_el = src_form_tree.getroot() src_form_el = src_form_tree.getroot()
form_version = src_form_el.get("version", format_version) # Borrowed form uses the extension's format version (not the source form's) — keeps the
# extension uniform; otherwise the platform rejects the import on a version mismatch
# (e.g. a 2.13 form inside a 2.17 extension). The platform upgrades the form to the root version.
form_version = format_version
src_auto_cmd = None src_auto_cmd = None
form_props = [] form_props = []
@@ -1149,25 +1246,21 @@ def main():
continue continue
if not reached_visual: if not reached_visual:
# Form-level properties before AutoCommandBar (WindowOpeningMode, AutoFillCheck, etc.) # Form-level properties before AutoCommandBar (WindowOpeningMode, AutoFillCheck, etc.)
form_props.append(etree.tostring(fc, encoding="unicode")) form_props.append(decode_numeric_entities(etree.tostring(fc, encoding="unicode")))
ns_strip_pattern = re.compile(r'\s+xmlns(?::\w+)?="[^"]*"') ns_strip_pattern = re.compile(r'\s+xmlns(?::\w+)?="[^"]*"')
# AutoCommandBar: keep ChildItems (buttons with CommandName->0), Autofill->false # AutoCommandBar: keep ChildItems (buttons with CommandName->0), Autofill->false
auto_cmd_xml = "" auto_cmd_xml = ""
if src_auto_cmd is not None: if src_auto_cmd is not None:
auto_cmd_xml = etree.tostring(src_auto_cmd, encoding="unicode") auto_cmd_xml = decode_numeric_entities(etree.tostring(src_auto_cmd, encoding="unicode"))
auto_cmd_xml = ns_strip_pattern.sub("", auto_cmd_xml) auto_cmd_xml = ns_strip_pattern.sub("", auto_cmd_xml)
auto_cmd_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', auto_cmd_xml) auto_cmd_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', auto_cmd_xml)
auto_cmd_xml = auto_cmd_xml.replace('<Autofill>true</Autofill>', '<Autofill>false</Autofill>') auto_cmd_xml = auto_cmd_xml.replace('<Autofill>true</Autofill>', '<Autofill>false</Autofill>')
# Strip ExcludedCommand (references to standard commands invalid in extension) # Strip ExcludedCommand (references to standard commands invalid in extension)
auto_cmd_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', auto_cmd_xml) auto_cmd_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', auto_cmd_xml)
# Strip DataPath in AutoCommandBar buttons # Strip data-binding tags whose root attribute isn't borrowed
if borrow_main_attr: auto_cmd_xml = strip_form_bindings(auto_cmd_xml, borrow_main_attr)
# Keep only Объект.* DataPaths
auto_cmd_xml = re.sub(r'\s*<DataPath>(?!\u041e\u0431\u044a\u0435\u043a\u0442\.)[^<]*</DataPath>', '', auto_cmd_xml)
else:
auto_cmd_xml = re.sub(r'\s*<DataPath>[^<]*</DataPath>', '', auto_cmd_xml)
# ChildItems: copy full tree, clean up base-config references # ChildItems: copy full tree, clean up base-config references
child_items_xml = "" child_items_xml = ""
@@ -1178,20 +1271,12 @@ def main():
break break
if src_child_items is not None: if src_child_items is not None:
child_items_xml = etree.tostring(src_child_items, encoding="unicode") child_items_xml = decode_numeric_entities(etree.tostring(src_child_items, encoding="unicode"))
child_items_xml = ns_strip_pattern.sub("", child_items_xml) child_items_xml = ns_strip_pattern.sub("", child_items_xml)
# Replace all CommandName values with 0 # Replace all CommandName values with 0
child_items_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', child_items_xml) child_items_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', child_items_xml)
# Strip DataPath / TitleDataPath / RowPictureDataPath # Strip data-binding tags whose root attribute isn't borrowed
if borrow_main_attr: child_items_xml = strip_form_bindings(child_items_xml, borrow_main_attr)
# Keep only Объект.* DataPaths — strip form-attribute DataPaths (not borrowed)
child_items_xml = re.sub(r'\s*<DataPath>(?!\u041e\u0431\u044a\u0435\u043a\u0442\.)[^<]*</DataPath>', '', child_items_xml)
child_items_xml = re.sub(r'\s*<TitleDataPath>(?!\u041e\u0431\u044a\u0435\u043a\u0442\.)[^<]*</TitleDataPath>', '', child_items_xml)
child_items_xml = re.sub(r'\s*<RowPictureDataPath>[^<]*</RowPictureDataPath>', '', child_items_xml)
else:
child_items_xml = re.sub(r'\s*<DataPath>[^<]*</DataPath>', '', child_items_xml)
child_items_xml = re.sub(r'\s*<TitleDataPath>[^<]*</TitleDataPath>', '', child_items_xml)
child_items_xml = re.sub(r'\s*<RowPictureDataPath>[^<]*</RowPictureDataPath>', '', child_items_xml)
# Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension) # Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension)
child_items_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', child_items_xml) child_items_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', child_items_xml)
# Strip TypeLink blocks with human-readable DataPath (Items.XXX) # Strip TypeLink blocks with human-readable DataPath (Items.XXX)
@@ -1428,12 +1513,16 @@ def main():
save_text_bom(form_xml_file, "".join(parts)) save_text_bom(form_xml_file, "".join(parts))
info(f" Created: {form_xml_file}") info(f" Created: {form_xml_file}")
# 6. Create empty Module.bsl # 6. Create empty Module.bsl — but NEVER overwrite an existing one (re-borrow must
# not clobber user code added to the form module).
module_dir = os.path.join(form_xml_dir, "Form") module_dir = os.path.join(form_xml_dir, "Form")
os.makedirs(module_dir, exist_ok=True) os.makedirs(module_dir, exist_ok=True)
module_bsl_file = os.path.join(module_dir, "Module.bsl") module_bsl_file = os.path.join(module_dir, "Module.bsl")
save_text_bom(module_bsl_file, "") if os.path.isfile(module_bsl_file):
info(f" Created: {module_bsl_file}") info(" Preserved existing Module.bsl")
else:
save_text_bom(module_bsl_file, "")
info(f" Created: {module_bsl_file}")
# 7. Register form in parent object ChildObjects # 7. Register form in parent object ChildObjects
register_form_in_object(type_name, obj_name, form_name) register_form_in_object(type_name, obj_name, form_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 "МоёРасширение"
``` ```
## Примеры ## Примеры
+13 -4
View File
@@ -1,4 +1,4 @@
# cfe-init v1.1 — Create 1C configuration extension scaffold (CFE) # cfe-init v1.2 — Create 1C configuration extension scaffold (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)]
@@ -35,6 +35,10 @@ if (Test-Path $cfgFile) {
exit 1 exit 1
} }
# MDClasses format version — inherited from the base config so the extension stays uniform
# with it (a 2.13 base must yield a 2.13 extension, else platform import rejects the mismatch).
$formatVersion = "2.17"
# --- Resolve ConfigPath --- # --- Resolve ConfigPath ---
$baseLangUuid = "00000000-0000-0000-0000-000000000000" $baseLangUuid = "00000000-0000-0000-0000-000000000000"
if ($ConfigPath) { if ($ConfigPath) {
@@ -75,6 +79,11 @@ if ($ConfigPath) {
$baseCfgDoc.Load((Resolve-Path $ConfigPath).Path) $baseCfgDoc.Load((Resolve-Path $ConfigPath).Path)
$baseCfgNs = New-Object System.Xml.XmlNamespaceManager($baseCfgDoc.NameTable) $baseCfgNs = New-Object System.Xml.XmlNamespaceManager($baseCfgDoc.NameTable)
$baseCfgNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") $baseCfgNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
$fmtVer = $baseCfgDoc.DocumentElement.GetAttribute("version")
if ($fmtVer) {
$formatVersion = $fmtVer
Write-Host "[INFO] Base config format version: $formatVersion"
}
$compatNode = $baseCfgDoc.SelectSingleNode("//md:Configuration/md:Properties/md:CompatibilityMode", $baseCfgNs) $compatNode = $baseCfgDoc.SelectSingleNode("//md:Configuration/md:Properties/md:CompatibilityMode", $baseCfgNs)
if ($compatNode -and $compatNode.InnerText) { if ($compatNode -and $compatNode.InnerText) {
$CompatibilityMode = $compatNode.InnerText.Trim() $CompatibilityMode = $compatNode.InnerText.Trim()
@@ -138,7 +147,7 @@ $childObjectsXml += "`r`n`t`t"
# --- 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>
@@ -203,7 +212,7 @@ $cfgXml = @"
# --- Languages/Русский.xml (adopted format) --- # --- Languages/Русский.xml (adopted format) ---
$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">
<InternalInfo/> <InternalInfo/>
<Properties> <Properties>
@@ -220,7 +229,7 @@ $langXml = @"
# --- Role XML --- # --- Role XML ---
$roleXml = @" $roleXml = @"
<?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">
<Role uuid="$uuidRole"> <Role uuid="$uuidRole">
<Properties> <Properties>
<Name>$([System.Security.SecurityElement]::Escape($roleName))</Name> <Name>$([System.Security.SecurityElement]::Escape($roleName))</Name>
+12 -4
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# cfe-init v1.1 — Create 1C configuration extension scaffold (CFE) # cfe-init v1.2 — Create 1C configuration extension scaffold (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Generates minimal XML source files for a 1C configuration extension.""" """Generates minimal XML source files for a 1C configuration extension."""
import sys, os, argparse, uuid import sys, os, argparse, uuid
@@ -50,6 +50,10 @@ def main():
print(f"Configuration.xml already exists: {cfg_file}", file=sys.stderr) print(f"Configuration.xml already exists: {cfg_file}", file=sys.stderr)
sys.exit(1) sys.exit(1)
# MDClasses format version — inherited from the base config so the extension stays uniform
# with it (a 2.13 base must yield a 2.13 extension, else platform import rejects the mismatch).
format_version = "2.17"
# --- Resolve ConfigPath --- # --- Resolve ConfigPath ---
base_lang_uuid = "00000000-0000-0000-0000-000000000000" base_lang_uuid = "00000000-0000-0000-0000-000000000000"
if args.ConfigPath: if args.ConfigPath:
@@ -88,6 +92,10 @@ def main():
try: try:
base_cfg_tree = ET.parse(os.path.abspath(config_path)) base_cfg_tree = ET.parse(os.path.abspath(config_path))
base_cfg_root = base_cfg_tree.getroot() base_cfg_root = base_cfg_tree.getroot()
fmt_ver = base_cfg_root.get("version")
if fmt_ver:
format_version = fmt_ver
print(f"[INFO] Base config format version: {format_version}")
ns = {'md': 'http://v8.1c.ru/8.3/MDClasses'} ns = {'md': 'http://v8.1c.ru/8.3/MDClasses'}
compat_node = base_cfg_root.find('.//md:Configuration/md:Properties/md:CompatibilityMode', ns) compat_node = base_cfg_root.find('.//md:Configuration/md:Properties/md:CompatibilityMode', ns)
if compat_node is not None and compat_node.text: if compat_node is not None and compat_node.text:
@@ -155,7 +163,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="{format_version}">
\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>
@@ -190,7 +198,7 @@ def main():
# --- Languages/Русский.xml (adopted format) --- # --- Languages/Русский.xml (adopted format) ---
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="{format_version}">
\t<Language uuid="{uuid_lang}"> \t<Language uuid="{uuid_lang}">
\t\t<InternalInfo/> \t\t<InternalInfo/>
\t\t<Properties> \t\t<Properties>
@@ -205,7 +213,7 @@ def main():
# --- Role XML --- # --- Role XML ---
role_xml = f'''<?xml version="1.0" encoding="UTF-8"?> role_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="{format_version}">
\t<Role uuid="{uuid_role}"> \t<Role uuid="{uuid_role}">
\t\t<Properties> \t\t<Properties>
\t\t\t<Name>{esc_xml(role_name)}</Name> \t\t\t<Name>{esc_xml(role_name)}</Name>
+1 -1
View File
@@ -51,7 +51,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-patch-method.ps1" -ExtensionPath src -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before python "${CLAUDE_SKILL_DIR}/scripts/cfe-patch-method.py" -ExtensionPath src -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"
``` ```
+7 -7
View File
@@ -25,20 +25,20 @@ allowed-tools:
## Параметры подключения ## Параметры подключения
Прочитай `.v8-project.json` из корня проекта для `v8path` (путь к платформе). Прочитай `.v8-project.json` из корня проекта для `v8path` (путь к платформе).
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
После создания базы предложи зарегистрировать через `/db-list add`. После создания базы предложи зарегистрировать через `/db-list add`.
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" <параметры> python "${CLAUDE_SKILL_DIR}/scripts/db-create.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | | `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Путь к файловой базе | | `-InfoBasePath <путь>` | * | Путь к файловой базе |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | | `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере | | `-InfoBaseRef <имя>` | * | Имя базы на сервере |
@@ -57,14 +57,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 "Новая база"
``` ```
+65 -4
View File
@@ -1,5 +1,6 @@
# db-create v1.1 — Create 1C information base # db-create v1.6 — Create 1C information base
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
.SYNOPSIS .SYNOPSIS
Создание информационной базы 1С Создание информационной базы 1С
@@ -96,7 +97,7 @@ if (-not $V8Path) {
$V8Path = $found.FullName $V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else { } else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
exit 1 exit 1
} }
} }
@@ -105,12 +106,47 @@ if (Test-Path $V8Path -PathType Container) {
} }
if (-not (Test-Path $V8Path)) { if (-not (Test-Path $V8Path)) {
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
exit 1 exit 1
} }
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
function Invoke-IbcmdProcess {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
param([string]$Exe, [string[]]$IbArgs)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $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.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd()
$err = $p.StandardError.ReadToEnd()
$p.WaitForExit()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
# --- Validate connection --- # --- Validate connection ---
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
exit 1
}
} elseif (-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
exit 1 exit 1
} }
@@ -126,6 +162,31 @@ $tempDir = Join-Path $env:TEMP "db_create_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
try { try {
if ($engine -eq "ibcmd") {
# --- ibcmd branch (file infobase only) ---
$arguments = @("infobase", "create", "--db-path=$InfoBasePath", "--create-database")
if ($UseTemplate) {
if ([System.IO.Path]::GetExtension($UseTemplate) -ieq ".dt") {
$arguments += "--restore=$UseTemplate"
} else {
$arguments += "--load=$UseTemplate", "--apply"
}
}
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $($arguments -join ' ')"
$__ib = Invoke-IbcmdProcess $V8Path $arguments
$output = $__ib.Output
$exitCode = $__ib.ExitCode
if ($exitCode -eq 0) {
Write-Host "Information base created successfully: $InfoBasePath" -ForegroundColor Green
} else {
Write-Host "Error creating information base (code: $exitCode)" -ForegroundColor Red
}
if ($output) { Write-Host ($output | Out-String) }
exit $exitCode
}
# --- 1cv8 branch ---
# --- Build arguments --- # --- Build arguments ---
$arguments = @("CREATEINFOBASE") $arguments = @("CREATEINFOBASE")
+81 -17
View File
@@ -1,8 +1,9 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-create v1.1 — Create 1C information base # db-create v1.6 — 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
import atexit
import glob import glob
import json import json
import os import os
@@ -35,36 +36,69 @@ def _find_project_v8path():
d = parent d = parent
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
if os.path.basename(parent).lower() == "bin":
parent = os.path.dirname(parent)
return os.path.basename(parent)
def _version_key(p): def _version_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe).""" """Numeric sort key from version dir name."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p))) return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path): def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe.""" """Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path: if not v8path:
v8path = _find_project_v8path() v8path = _find_project_v8path()
if not v8path: if not v8path:
candidates = ( if os.name == "nt":
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe") candidates = (
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
) + glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
else:
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
candidates = glob.glob("/opt/1cv8/*/1cv8")
if candidates: if candidates:
v8path = max(candidates, key=_version_key) v8path = max(candidates, key=_version_key)
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path))) print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
print(f"Auto-selected platform {ver}: {v8path}")
else: else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1) sys.exit(1)
if os.path.isdir(v8path): if os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe") # PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
v8path = os.path.join(v8path, exe)
if not os.path.isfile(v8path): if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr) print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
sys.exit(1) sys.exit(1)
return v8path return v8path
IBCMD_NOUSER_HINT = (
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
"call may block instead of failing. If it does not return promptly, abort and "
"re-run with -UserName and -Password.\n"
)
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
On Windows without -UserName ibcmd reads the console directly and may still block —
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
"""
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
def main(): def main():
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
@@ -82,9 +116,14 @@ def main():
args = parser.parse_args() args = parser.parse_args()
v8path = resolve_v8path(args.V8Path) v8path = resolve_v8path(args.V8Path)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Validate connection --- # --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): if engine == "ibcmd":
if not args.InfoBasePath:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
sys.exit(1)
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr) print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1) sys.exit(1)
@@ -93,6 +132,29 @@ def main():
print(f"Error: template file not found: {args.UseTemplate}", file=sys.stderr) print(f"Error: template file not found: {args.UseTemplate}", file=sys.stderr)
sys.exit(1) sys.exit(1)
# --- ibcmd branch (file infobase only) ---
if engine == "ibcmd":
arguments = ["infobase", "create", f"--db-path={args.InfoBasePath}", "--create-database"]
if args.UseTemplate:
if os.path.splitext(args.UseTemplate)[1].lower() == ".dt":
arguments.append(f"--restore={args.UseTemplate}")
else:
arguments.extend([f"--load={args.UseTemplate}", "--apply"])
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(arguments)}")
result = run_ibcmd([v8path] + arguments, warn_no_user=False)
if result.returncode == 0:
print(f"Information base created successfully: {args.InfoBasePath}")
else:
print(f"Error creating information base (code: {result.returncode})", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
sys.exit(result.returncode)
# --- Temp dir --- # --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_create_{random.randint(0, 999999)}") temp_dir = os.path.join(tempfile.gettempdir(), f"db_create_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True) os.makedirs(temp_dir, exist_ok=True)
@@ -102,9 +164,11 @@ def main():
arguments = ["CREATEINFOBASE"] arguments = ["CREATEINFOBASE"]
if args.InfoBaseServer and args.InfoBaseRef: if args.InfoBaseServer and args.InfoBaseRef:
arguments.append(f'Srvr="{args.InfoBaseServer}";Ref="{args.InfoBaseRef}"') # No embedded quotes: subprocess quotes the whole token; 1C's argv parser
# 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:
+6 -6
View File
@@ -28,21 +28,21 @@ allowed-tools:
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` 2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default` 4. Если ветка не совпала — используй `default`
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если файла нет — предложи `/db-list add`. Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" <параметры> python "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | | `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Файловая база | | `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | | `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере | | `-InfoBaseRef <имя>` | * | Имя базы на сервере |
@@ -58,11 +58,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 "МоёРасширение"
``` ```
@@ -1,5 +1,6 @@
# db-dump-cf v1.1 — Dump 1C configuration to CF file # db-dump-cf v1.6 — Dump 1C configuration to CF file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
.SYNOPSIS .SYNOPSIS
Выгрузка конфигурации 1С в CF-файл Выгрузка конфигурации 1С в CF-файл
@@ -105,7 +106,7 @@ if (-not $V8Path) {
$V8Path = $found.FullName $V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else { } else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
exit 1 exit 1
} }
} }
@@ -114,12 +115,47 @@ if (Test-Path $V8Path -PathType Container) {
} }
if (-not (Test-Path $V8Path)) { if (-not (Test-Path $V8Path)) {
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
exit 1 exit 1
} }
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
function Invoke-IbcmdProcess {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
param([string]$Exe, [string[]]$IbArgs)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $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.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd()
$err = $p.StandardError.ReadToEnd()
$p.WaitForExit()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
# --- Validate connection --- # --- Validate connection ---
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
exit 1
}
} elseif (-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
exit 1 exit 1
} }
@@ -135,6 +171,32 @@ $tempDir = Join-Path $env:TEMP "db_dump_cf_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
try { try {
if ($engine -eq "ibcmd") {
# --- ibcmd branch (file infobase only) ---
if ($AllExtensions) {
Write-Host "Error: ibcmd config save does not support -AllExtensions (use -Extension)" -ForegroundColor Red
exit 1
}
$arguments = @("infobase", "config", "save", "--db-path=$InfoBasePath")
if ($Extension) { $arguments += "--extension=$Extension" }
$arguments += "$OutputFile"
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $($arguments -join ' ')"
$__ib = Invoke-IbcmdProcess $V8Path $arguments
$output = $__ib.Output
$exitCode = $__ib.ExitCode
if ($exitCode -eq 0) {
Write-Host "Configuration dumped successfully to: $OutputFile" -ForegroundColor Green
} else {
Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red
}
if ($output) { Write-Host ($output | Out-String) }
exit $exitCode
}
# --- 1cv8 branch ---
# --- Build arguments --- # --- Build arguments ---
$arguments = @("DESIGNER") $arguments = @("DESIGNER")
+82 -15
View File
@@ -1,8 +1,9 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-dump-cf v1.1 — Dump 1C configuration to CF file # db-dump-cf v1.6 — 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
import atexit
import glob import glob
import json import json
import os import os
@@ -35,36 +36,69 @@ def _find_project_v8path():
d = parent d = parent
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
if os.path.basename(parent).lower() == "bin":
parent = os.path.dirname(parent)
return os.path.basename(parent)
def _version_key(p): def _version_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe).""" """Numeric sort key from version dir name."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p))) return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path): def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe.""" """Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path: if not v8path:
v8path = _find_project_v8path() v8path = _find_project_v8path()
if not v8path: if not v8path:
candidates = ( if os.name == "nt":
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe") candidates = (
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
) + glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
else:
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
candidates = glob.glob("/opt/1cv8/*/1cv8")
if candidates: if candidates:
v8path = max(candidates, key=_version_key) v8path = max(candidates, key=_version_key)
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path))) print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
print(f"Auto-selected platform {ver}: {v8path}")
else: else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1) sys.exit(1)
if os.path.isdir(v8path): if os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe") # PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
v8path = os.path.join(v8path, exe)
if not os.path.isfile(v8path): if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr) print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
sys.exit(1) sys.exit(1)
return v8path return v8path
IBCMD_NOUSER_HINT = (
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
"call may block instead of failing. If it does not return promptly, abort and "
"re-run with -UserName and -Password.\n"
)
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
On Windows without -UserName ibcmd reads the console directly and may still block —
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
"""
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
def main(): def main():
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
@@ -84,9 +118,14 @@ def main():
args = parser.parse_args() args = parser.parse_args()
v8path = resolve_v8path(args.V8Path) v8path = resolve_v8path(args.V8Path)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Validate connection --- # --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): if engine == "ibcmd":
if not args.InfoBasePath:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
sys.exit(1)
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr) print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1) sys.exit(1)
@@ -95,6 +134,34 @@ def main():
if out_dir and not os.path.isdir(out_dir): if out_dir and not os.path.isdir(out_dir):
os.makedirs(out_dir, exist_ok=True) os.makedirs(out_dir, exist_ok=True)
# --- ibcmd branch (file infobase only) ---
if engine == "ibcmd":
if args.AllExtensions:
print("Error: ibcmd config save does not support -AllExtensions (use -Extension)", file=sys.stderr)
sys.exit(1)
arguments = ["infobase", "config", "save", f"--db-path={args.InfoBasePath}"]
if args.Extension:
arguments.append(f"--extension={args.Extension}")
arguments.append(args.OutputFile)
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
if args.UserName:
arguments.append(f"--user={args.UserName}")
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(arguments)}")
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
if result.returncode == 0:
print(f"Configuration dumped successfully to: {args.OutputFile}")
else:
print(f"Error dumping configuration (code: {result.returncode})", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
sys.exit(result.returncode)
# --- Temp dir --- # --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_cf_{random.randint(0, 999999)}") temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_cf_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True) os.makedirs(temp_dir, exist_ok=True)
+5 -5
View File
@@ -31,21 +31,21 @@ allowed-tools:
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` 2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default` 4. Если ветка не совпала — используй `default`
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если файла нет — предложи `/db-list add`. Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" <параметры> python "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | | `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Файловая база | | `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | | `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере | | `-InfoBaseRef <имя>` | * | Имя базы на сервере |
@@ -59,10 +59,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"
``` ```
## Связанные навыки ## Связанные навыки
@@ -1,5 +1,6 @@
# db-dump-dt v1.1 — Dump 1C information base to DT file # db-dump-dt v1.5 — Dump 1C information base to DT file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
.SYNOPSIS .SYNOPSIS
Выгрузка информационной базы 1С в DT-файл Выгрузка информационной базы 1С в DT-файл
@@ -89,7 +90,7 @@ if (-not $V8Path) {
$V8Path = $found.FullName $V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else { } else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
exit 1 exit 1
} }
} }
@@ -98,12 +99,47 @@ if (Test-Path $V8Path -PathType Container) {
} }
if (-not (Test-Path $V8Path)) { if (-not (Test-Path $V8Path)) {
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
exit 1 exit 1
} }
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
function Invoke-IbcmdProcess {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
param([string]$Exe, [string[]]$IbArgs)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $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.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd()
$err = $p.StandardError.ReadToEnd()
$p.WaitForExit()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
# --- Validate connection --- # --- Validate connection ---
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
exit 1
}
} elseif (-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
exit 1 exit 1
} }
@@ -119,6 +155,28 @@ $tempDir = Join-Path $env:TEMP "db_dump_dt_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
try { try {
if ($engine -eq "ibcmd") {
# --- ibcmd branch (file infobase only) ---
$arguments = @("infobase", "dump", "--db-path=$InfoBasePath")
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "$OutputFile"
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $($arguments -join ' ')"
$__ib = Invoke-IbcmdProcess $V8Path $arguments
$output = $__ib.Output
$exitCode = $__ib.ExitCode
if ($exitCode -eq 0) {
Write-Host "Information base dumped successfully to: $OutputFile" -ForegroundColor Green
} else {
Write-Host "Error dumping information base (code: $exitCode)" -ForegroundColor Red
}
if ($output) { Write-Host ($output | Out-String) }
exit $exitCode
}
# --- 1cv8 branch ---
# --- Build arguments --- # --- Build arguments ---
$arguments = @("DESIGNER") $arguments = @("DESIGNER")
+77 -15
View File
@@ -1,8 +1,9 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-dump-dt v1.1 — Dump 1C information base to DT file # db-dump-dt v1.5 — 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
import atexit
import glob import glob
import json import json
import os import os
@@ -35,36 +36,69 @@ def _find_project_v8path():
d = parent d = parent
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
if os.path.basename(parent).lower() == "bin":
parent = os.path.dirname(parent)
return os.path.basename(parent)
def _version_key(p): def _version_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe).""" """Numeric sort key from version dir name."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p))) return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path): def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe.""" """Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path: if not v8path:
v8path = _find_project_v8path() v8path = _find_project_v8path()
if not v8path: if not v8path:
candidates = ( if os.name == "nt":
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe") candidates = (
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
) + glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
else:
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
candidates = glob.glob("/opt/1cv8/*/1cv8")
if candidates: if candidates:
v8path = max(candidates, key=_version_key) v8path = max(candidates, key=_version_key)
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path))) print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
print(f"Auto-selected platform {ver}: {v8path}")
else: else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1) sys.exit(1)
if os.path.isdir(v8path): if os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe") # PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
v8path = os.path.join(v8path, exe)
if not os.path.isfile(v8path): if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr) print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
sys.exit(1) sys.exit(1)
return v8path return v8path
IBCMD_NOUSER_HINT = (
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
"call may block instead of failing. If it does not return promptly, abort and "
"re-run with -UserName and -Password.\n"
)
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
On Windows without -UserName ibcmd reads the console directly and may still block —
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
"""
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
def main(): def main():
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
@@ -82,9 +116,14 @@ def main():
args = parser.parse_args() args = parser.parse_args()
v8path = resolve_v8path(args.V8Path) v8path = resolve_v8path(args.V8Path)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Validate connection --- # --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): if engine == "ibcmd":
if not args.InfoBasePath:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
sys.exit(1)
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr) print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1) sys.exit(1)
@@ -93,6 +132,29 @@ def main():
if out_dir and not os.path.isdir(out_dir): if out_dir and not os.path.isdir(out_dir):
os.makedirs(out_dir, exist_ok=True) os.makedirs(out_dir, exist_ok=True)
# --- ibcmd branch (file infobase only) ---
if engine == "ibcmd":
arguments = ["infobase", "dump", f"--db-path={args.InfoBasePath}"]
if args.UserName:
arguments.append(f"--user={args.UserName}")
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(args.OutputFile)
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(arguments)}")
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
if result.returncode == 0:
print(f"Information base dumped successfully to: {args.OutputFile}")
else:
print(f"Error dumping information base (code: {result.returncode})", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
sys.exit(result.returncode)
# --- Temp dir --- # --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_dt_{random.randint(0, 999999)}") temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_dt_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True) os.makedirs(temp_dir, exist_ok=True)
+8 -8
View File
@@ -29,7 +29,7 @@ allowed-tools:
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` 2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default` 4. Если ветка не совпала — используй `default`
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если файла нет — предложи `/db-list add`. Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
Если в записи базы указан `configSrc` — используй как каталог выгрузки по умолчанию. Если в записи базы указан `configSrc` — используй как каталог выгрузки по умолчанию.
@@ -37,14 +37,14 @@ 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" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | | `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Файловая база | | `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | | `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере | | `-InfoBaseRef <имя>` | * | Имя базы на сервере |
@@ -74,17 +74,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,5 +1,6 @@
# db-dump-xml v1.1 — Dump 1C configuration to XML files # db-dump-xml v1.8 — Dump 1C configuration to XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
.SYNOPSIS .SYNOPSIS
Выгрузка конфигурации 1С в XML-файлы Выгрузка конфигурации 1С в XML-файлы
@@ -128,7 +129,7 @@ if (-not $V8Path) {
$V8Path = $found.FullName $V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else { } else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
exit 1 exit 1
} }
} }
@@ -137,12 +138,47 @@ if (Test-Path $V8Path -PathType Container) {
} }
if (-not (Test-Path $V8Path)) { if (-not (Test-Path $V8Path)) {
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
exit 1 exit 1
} }
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
function Invoke-IbcmdProcess {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
param([string]$Exe, [string[]]$IbArgs)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $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.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd()
$err = $p.StandardError.ReadToEnd()
$p.WaitForExit()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
# --- Validate connection --- # --- Validate connection ---
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
exit 1
}
} elseif (-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
exit 1 exit 1
} }
@@ -164,6 +200,44 @@ $tempDir = Join-Path $env:TEMP "db_dump_xml_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
try { try {
if ($engine -eq "ibcmd") {
# --- ibcmd branch (file infobase only; hierarchical Full/Changes) ---
if ($Format -eq "Plain") {
Write-Host "Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red
exit 1
}
if ($AllExtensions) {
$arguments = @("infobase", "config", "export", "all-extensions", "$ConfigDir", "--db-path=$InfoBasePath")
} elseif ($Mode -eq "UpdateInfo") {
Write-Host "Error: ibcmd config export does not support Mode UpdateInfo; use 1cv8" -ForegroundColor Red
exit 1
} elseif ($Mode -eq "Partial") {
$objList = @($Objects -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
$arguments = @("infobase", "config", "export", "objects") + $objList
$arguments += "--out=$ConfigDir", "--db-path=$InfoBasePath"
if ($Extension) { $arguments += "--extension=$Extension" }
} else {
$arguments = @("infobase", "config", "export", "--db-path=$InfoBasePath")
if ($Extension) { $arguments += "--extension=$Extension" }
$arguments += "$ConfigDir"
}
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $($arguments -join ' ')"
$__ib = Invoke-IbcmdProcess $V8Path $arguments
$output = $__ib.Output
$exitCode = $__ib.ExitCode
if ($exitCode -eq 0) {
Write-Host "Configuration exported successfully to: $ConfigDir" -ForegroundColor Green
} else {
Write-Host "Error exporting configuration (code: $exitCode)" -ForegroundColor Red
}
if ($output) { Write-Host ($output | Out-String) }
exit $exitCode
}
# --- 1cv8 branch ---
# --- Build arguments --- # --- Build arguments ---
$arguments = @("DESIGNER") $arguments = @("DESIGNER")
@@ -1,8 +1,9 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-dump-xml v1.1 — Dump 1C configuration to XML files # db-dump-xml v1.8 — 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
import atexit
import glob import glob
import json import json
import os import os
@@ -35,36 +36,69 @@ def _find_project_v8path():
d = parent d = parent
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
if os.path.basename(parent).lower() == "bin":
parent = os.path.dirname(parent)
return os.path.basename(parent)
def _version_key(p): def _version_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe).""" """Numeric sort key from version dir name."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p))) return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path): def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe.""" """Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path: if not v8path:
v8path = _find_project_v8path() v8path = _find_project_v8path()
if not v8path: if not v8path:
candidates = ( if os.name == "nt":
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe") candidates = (
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
) + glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
else:
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
candidates = glob.glob("/opt/1cv8/*/1cv8")
if candidates: if candidates:
v8path = max(candidates, key=_version_key) v8path = max(candidates, key=_version_key)
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path))) print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
print(f"Auto-selected platform {ver}: {v8path}")
else: else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1) sys.exit(1)
if os.path.isdir(v8path): if os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe") # PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
v8path = os.path.join(v8path, exe)
if not os.path.isfile(v8path): if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr) print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
sys.exit(1) sys.exit(1)
return v8path return v8path
IBCMD_NOUSER_HINT = (
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
"call may block instead of failing. If it does not return promptly, abort and "
"re-run with -UserName and -Password.\n"
)
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
On Windows without -UserName ibcmd reads the console directly and may still block —
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
"""
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
def main(): def main():
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
@@ -98,9 +132,14 @@ def main():
# --- 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"
# --- Validate connection --- # --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): if engine == "ibcmd":
if not args.InfoBasePath:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
sys.exit(1)
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr) print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1) sys.exit(1)
@@ -114,6 +153,46 @@ def main():
os.makedirs(args.ConfigDir, exist_ok=True) os.makedirs(args.ConfigDir, exist_ok=True)
print(f"Created output directory: {args.ConfigDir}") print(f"Created output directory: {args.ConfigDir}")
# --- ibcmd branch (file infobase only; hierarchical Full/Changes) ---
if engine == "ibcmd":
if args.Format == "Plain":
print("Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)", file=sys.stderr)
sys.exit(1)
if args.AllExtensions:
arguments = ["infobase", "config", "export", "all-extensions", args.ConfigDir, f"--db-path={args.InfoBasePath}"]
elif args.Mode == "UpdateInfo":
print("Error: ibcmd config export does not support Mode UpdateInfo; use 1cv8", file=sys.stderr)
sys.exit(1)
elif args.Mode == "Partial":
obj_list = [o.strip() for o in args.Objects.split(",") if o.strip()]
arguments = ["infobase", "config", "export", "objects"] + obj_list
arguments += [f"--out={args.ConfigDir}", f"--db-path={args.InfoBasePath}"]
if args.Extension:
arguments.append(f"--extension={args.Extension}")
else:
arguments = ["infobase", "config", "export", f"--db-path={args.InfoBasePath}"]
if args.Extension:
arguments.append(f"--extension={args.Extension}")
arguments.append(args.ConfigDir)
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
if args.UserName:
arguments.append(f"--user={args.UserName}")
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(arguments)}")
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
if result.returncode == 0:
print(f"Configuration exported successfully to: {args.ConfigDir}")
else:
print(f"Error exporting configuration (code: {result.returncode})", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
sys.exit(result.returncode)
# --- Temp dir --- # --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_xml_{random.randint(0, 999999)}") temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_xml_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True) os.makedirs(temp_dir, exist_ok=True)
+6 -6
View File
@@ -29,21 +29,21 @@ allowed-tools:
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` 2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default` 4. Если ветка не совпала — используй `default`
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если файла нет — предложи `/db-list add`. Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" <параметры> python "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | | `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Файловая база | | `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | | `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере | | `-InfoBaseRef <имя>` | * | Имя базы на сервере |
@@ -63,11 +63,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 "МоёРасширение"
``` ```
@@ -1,5 +1,6 @@
# db-load-cf v1.1 — Load 1C configuration from CF file # db-load-cf v1.6 — Load 1C configuration from CF file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
.SYNOPSIS .SYNOPSIS
Загрузка конфигурации 1С из CF-файла Загрузка конфигурации 1С из CF-файла
@@ -105,7 +106,7 @@ if (-not $V8Path) {
$V8Path = $found.FullName $V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else { } else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
exit 1 exit 1
} }
} }
@@ -114,12 +115,47 @@ if (Test-Path $V8Path -PathType Container) {
} }
if (-not (Test-Path $V8Path)) { if (-not (Test-Path $V8Path)) {
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
exit 1 exit 1
} }
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
function Invoke-IbcmdProcess {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
param([string]$Exe, [string[]]$IbArgs)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $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.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd()
$err = $p.StandardError.ReadToEnd()
$p.WaitForExit()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
# --- Validate connection --- # --- Validate connection ---
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
exit 1
}
} elseif (-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
exit 1 exit 1
} }
@@ -135,6 +171,32 @@ $tempDir = Join-Path $env:TEMP "db_load_cf_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
try { try {
if ($engine -eq "ibcmd") {
# --- ibcmd branch (file infobase only) ---
if ($AllExtensions) {
Write-Host "Error: ibcmd config load does not support -AllExtensions (use -Extension)" -ForegroundColor Red
exit 1
}
$arguments = @("infobase", "config", "load", "--db-path=$InfoBasePath")
if ($Extension) { $arguments += "--extension=$Extension" }
$arguments += "$InputFile"
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $($arguments -join ' ')"
$__ib = Invoke-IbcmdProcess $V8Path $arguments
$output = $__ib.Output
$exitCode = $__ib.ExitCode
if ($exitCode -eq 0) {
Write-Host "Configuration loaded successfully from: $InputFile" -ForegroundColor Green
} else {
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red
}
if ($output) { Write-Host ($output | Out-String) }
exit $exitCode
}
# --- 1cv8 branch ---
# --- Build arguments --- # --- Build arguments ---
$arguments = @("DESIGNER") $arguments = @("DESIGNER")
+82 -15
View File
@@ -1,8 +1,9 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-load-cf v1.1 — Load 1C configuration from CF file # db-load-cf v1.6 — 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
import atexit
import glob import glob
import json import json
import os import os
@@ -35,36 +36,69 @@ def _find_project_v8path():
d = parent d = parent
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
if os.path.basename(parent).lower() == "bin":
parent = os.path.dirname(parent)
return os.path.basename(parent)
def _version_key(p): def _version_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe).""" """Numeric sort key from version dir name."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p))) return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path): def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe.""" """Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path: if not v8path:
v8path = _find_project_v8path() v8path = _find_project_v8path()
if not v8path: if not v8path:
candidates = ( if os.name == "nt":
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe") candidates = (
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
) + glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
else:
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
candidates = glob.glob("/opt/1cv8/*/1cv8")
if candidates: if candidates:
v8path = max(candidates, key=_version_key) v8path = max(candidates, key=_version_key)
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path))) print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
print(f"Auto-selected platform {ver}: {v8path}")
else: else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1) sys.exit(1)
if os.path.isdir(v8path): if os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe") # PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
v8path = os.path.join(v8path, exe)
if not os.path.isfile(v8path): if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr) print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
sys.exit(1) sys.exit(1)
return v8path return v8path
IBCMD_NOUSER_HINT = (
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
"call may block instead of failing. If it does not return promptly, abort and "
"re-run with -UserName and -Password.\n"
)
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
On Windows without -UserName ibcmd reads the console directly and may still block —
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
"""
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
def main(): def main():
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
@@ -84,9 +118,14 @@ def main():
args = parser.parse_args() args = parser.parse_args()
v8path = resolve_v8path(args.V8Path) v8path = resolve_v8path(args.V8Path)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Validate connection --- # --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): if engine == "ibcmd":
if not args.InfoBasePath:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
sys.exit(1)
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr) print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1) sys.exit(1)
@@ -95,6 +134,34 @@ def main():
print(f"Error: input file not found: {args.InputFile}", file=sys.stderr) print(f"Error: input file not found: {args.InputFile}", file=sys.stderr)
sys.exit(1) sys.exit(1)
# --- ibcmd branch (file infobase only) ---
if engine == "ibcmd":
if args.AllExtensions:
print("Error: ibcmd config load does not support -AllExtensions (use -Extension)", file=sys.stderr)
sys.exit(1)
arguments = ["infobase", "config", "load", f"--db-path={args.InfoBasePath}"]
if args.Extension:
arguments.append(f"--extension={args.Extension}")
arguments.append(args.InputFile)
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
if args.UserName:
arguments.append(f"--user={args.UserName}")
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(arguments)}")
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
if result.returncode == 0:
print(f"Configuration loaded successfully from: {args.InputFile}")
else:
print(f"Error loading configuration (code: {result.returncode})", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
sys.exit(result.returncode)
# --- Temp dir --- # --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_cf_{random.randint(0, 999999)}") temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_cf_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True) os.makedirs(temp_dir, exist_ok=True)
+5 -5
View File
@@ -45,21 +45,21 @@ allowed-tools:
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` 2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default` 4. Если ветка не совпала — используй `default`
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если файла нет — предложи `/db-list add`. Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" <параметры> python "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | | `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Файловая база | | `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | | `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере | | `-InfoBaseRef <имя>` | * | Имя базы на сервере |
@@ -80,10 +80,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
``` ```
## Связанные навыки ## Связанные навыки
@@ -1,5 +1,6 @@
# db-load-dt v1.1 — Load 1C information base from DT file # db-load-dt v1.5 — Load 1C information base from DT file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
.SYNOPSIS .SYNOPSIS
Загрузка информационной базы 1С из DT-файла Загрузка информационной базы 1С из DT-файла
@@ -102,7 +103,7 @@ if (-not $V8Path) {
$V8Path = $found.FullName $V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else { } else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
exit 1 exit 1
} }
} }
@@ -111,12 +112,47 @@ if (Test-Path $V8Path -PathType Container) {
} }
if (-not (Test-Path $V8Path)) { if (-not (Test-Path $V8Path)) {
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
exit 1 exit 1
} }
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
function Invoke-IbcmdProcess {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
param([string]$Exe, [string[]]$IbArgs)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $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.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd()
$err = $p.StandardError.ReadToEnd()
$p.WaitForExit()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
# --- Validate connection --- # --- Validate connection ---
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
exit 1
}
} elseif (-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
exit 1 exit 1
} }
@@ -132,6 +168,29 @@ $tempDir = Join-Path $env:TEMP "db_load_dt_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
try { try {
if ($engine -eq "ibcmd") {
# --- ibcmd branch (file infobase only) ---
$arguments = @("infobase", "restore", "--db-path=$InfoBasePath")
if (-not (Test-Path (Join-Path $InfoBasePath "1Cv8.1CD"))) { $arguments += "--create-database" }
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "$InputFile"
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $($arguments -join ' ')"
$__ib = Invoke-IbcmdProcess $V8Path $arguments
$output = $__ib.Output
$exitCode = $__ib.ExitCode
if ($exitCode -eq 0) {
Write-Host "Information base restored successfully from: $InputFile" -ForegroundColor Green
} else {
Write-Host "Error restoring information base (code: $exitCode)" -ForegroundColor Red
}
if ($output) { Write-Host ($output | Out-String) }
exit $exitCode
}
# --- 1cv8 branch ---
# --- Build arguments --- # --- Build arguments ---
$arguments = @("DESIGNER") $arguments = @("DESIGNER")
+79 -15
View File
@@ -1,8 +1,9 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-load-dt v1.1 — Load 1C information base from DT file # db-load-dt v1.5 — 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
import atexit
import glob import glob
import json import json
import os import os
@@ -35,36 +36,69 @@ def _find_project_v8path():
d = parent d = parent
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
if os.path.basename(parent).lower() == "bin":
parent = os.path.dirname(parent)
return os.path.basename(parent)
def _version_key(p): def _version_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe).""" """Numeric sort key from version dir name."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p))) return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path): def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe.""" """Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path: if not v8path:
v8path = _find_project_v8path() v8path = _find_project_v8path()
if not v8path: if not v8path:
candidates = ( if os.name == "nt":
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe") candidates = (
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
) + glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
else:
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
candidates = glob.glob("/opt/1cv8/*/1cv8")
if candidates: if candidates:
v8path = max(candidates, key=_version_key) v8path = max(candidates, key=_version_key)
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path))) print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
print(f"Auto-selected platform {ver}: {v8path}")
else: else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1) sys.exit(1)
if os.path.isdir(v8path): if os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe") # PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
v8path = os.path.join(v8path, exe)
if not os.path.isfile(v8path): if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr) print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
sys.exit(1) sys.exit(1)
return v8path return v8path
IBCMD_NOUSER_HINT = (
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
"call may block instead of failing. If it does not return promptly, abort and "
"re-run with -UserName and -Password.\n"
)
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
On Windows without -UserName ibcmd reads the console directly and may still block —
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
"""
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
def main(): def main():
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
@@ -84,9 +118,14 @@ def main():
args = parser.parse_args() args = parser.parse_args()
v8path = resolve_v8path(args.V8Path) v8path = resolve_v8path(args.V8Path)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Validate connection --- # --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): if engine == "ibcmd":
if not args.InfoBasePath:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
sys.exit(1)
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr) print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1) sys.exit(1)
@@ -95,6 +134,31 @@ def main():
print(f"Error: input file not found: {args.InputFile}", file=sys.stderr) print(f"Error: input file not found: {args.InputFile}", file=sys.stderr)
sys.exit(1) sys.exit(1)
# --- ibcmd branch (file infobase only) ---
if engine == "ibcmd":
arguments = ["infobase", "restore", f"--db-path={args.InfoBasePath}"]
if not os.path.isfile(os.path.join(args.InfoBasePath, "1Cv8.1CD")):
arguments.append("--create-database")
if args.UserName:
arguments.append(f"--user={args.UserName}")
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(args.InputFile)
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(arguments)}")
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
if result.returncode == 0:
print(f"Information base restored successfully from: {args.InputFile}")
else:
print(f"Error restoring information base (code: {result.returncode})", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
sys.exit(result.returncode)
# --- Temp dir --- # --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_dt_{random.randint(0, 999999)}") temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_dt_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True) os.makedirs(temp_dir, exist_ok=True)
+5 -5
View File
@@ -30,7 +30,7 @@ allowed-tools:
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` 2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default` 4. Если ветка не совпала — используй `default`
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если файла нет — предложи `/db-list add`. Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
Если в записи базы указан `configSrc` — используй как каталог конфигурации. Если в записи базы указан `configSrc` — используй как каталог конфигурации.
@@ -38,14 +38,14 @@ 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" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | | `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Файловая база | | `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | | `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере | | `-InfoBaseRef <имя>` | * | Имя базы на сервере |
@@ -70,8 +70,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,5 +1,6 @@
# db-load-git v1.5 — Load Git changes into 1C database # db-load-git v1.11 — Load Git changes into 1C database
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
.SYNOPSIS .SYNOPSIS
Загрузка изменений из Git в базу 1С Загрузка изменений из Git в базу 1С
@@ -149,7 +150,7 @@ if (-not $DryRun) {
$V8Path = $found.FullName $V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else { } else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
exit 1 exit 1
} }
} }
@@ -158,14 +159,48 @@ if (-not $DryRun) {
} }
if (-not (Test-Path $V8Path)) { if (-not (Test-Path $V8Path)) {
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
exit 1 exit 1
} }
} }
# --- Validate connection (skip if DryRun) --- # --- Detect engine + validate connection (skip if DryRun) ---
$engine = "1cv8"
if (-not $DryRun) { if (-not $DryRun) {
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { function Invoke-IbcmdProcess {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
param([string]$Exe, [string[]]$IbArgs)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $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.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd()
$err = $p.StandardError.ReadToEnd()
$p.WaitForExit()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
exit 1
}
} elseif (-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
exit 1 exit 1
} }
@@ -192,6 +227,15 @@ try {
} }
# --- Get changed files from Git --- # --- Get changed files from Git ---
# Все git-вызовы для сбора путей идут через один хелпер с -c core.quotePath=false,
# иначе кириллические пути возвращаются в octal-виде и не распознаются (зеркало run_git в .py).
function Invoke-GitLines {
param([string[]]$GitArgs)
$out = git -c core.quotePath=false @GitArgs 2>&1
if ($LASTEXITCODE -eq 0) { return $out }
return @()
}
$changedFiles = @() $changedFiles = @()
$ConfigDir = (Resolve-Path $ConfigDir).Path.TrimEnd('\') $ConfigDir = (Resolve-Path $ConfigDir).Path.TrimEnd('\')
$configDirNormalized = $ConfigDir.Replace('\', '/') $configDirNormalized = $ConfigDir.Replace('\', '/')
@@ -201,29 +245,22 @@ try {
switch ($Source) { switch ($Source) {
"Staged" { "Staged" {
Write-Host "Getting staged changes..." Write-Host "Getting staged changes..."
$raw = git diff --cached --name-only --relative 2>&1 $changedFiles += Invoke-GitLines -GitArgs @('diff', '--cached', '--name-only', '--relative')
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
} }
"Unstaged" { "Unstaged" {
Write-Host "Getting unstaged changes..." Write-Host "Getting unstaged changes..."
$raw = git diff --name-only --relative 2>&1 $changedFiles += Invoke-GitLines -GitArgs @('diff', '--name-only', '--relative')
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw } $changedFiles += Invoke-GitLines -GitArgs @('ls-files', '--others', '--exclude-standard')
$raw = git ls-files --others --exclude-standard 2>&1
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
} }
"Commit" { "Commit" {
Write-Host "Getting changes from $CommitRange..." Write-Host "Getting changes from $CommitRange..."
$raw = git diff --name-only --relative $CommitRange 2>&1 $changedFiles += Invoke-GitLines -GitArgs @('diff', '--name-only', '--relative', $CommitRange)
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
} }
"All" { "All" {
Write-Host "Getting all uncommitted changes..." Write-Host "Getting all uncommitted changes..."
$raw = git diff --cached --name-only --relative 2>&1 $changedFiles += Invoke-GitLines -GitArgs @('diff', '--cached', '--name-only', '--relative')
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw } $changedFiles += Invoke-GitLines -GitArgs @('diff', '--name-only', '--relative')
$raw = git diff --name-only --relative 2>&1 $changedFiles += Invoke-GitLines -GitArgs @('ls-files', '--others', '--exclude-standard')
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
$raw = git ls-files --others --exclude-standard 2>&1
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
} }
} }
} finally { } finally {
@@ -319,6 +356,53 @@ $tempDir = Join-Path $env:TEMP "db_load_git_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
try { try {
if ($engine -eq "ibcmd") {
# --- ibcmd branch (file infobase only; import specific files) ---
if ($Format -eq "Plain") {
Write-Host "Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red
exit 1
}
if ($AllExtensions) {
Write-Host "Error: ibcmd config import does not support -AllExtensions (use -Extension or 1cv8)" -ForegroundColor Red
exit 1
}
$arguments = @("infobase", "config", "import", "files") + $configFiles
$arguments += "--base-dir=$ConfigDir", "--db-path=$InfoBasePath"
if ($Extension) { $arguments += "--extension=$Extension" }
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $($arguments -join ' ')"
$__ib = Invoke-IbcmdProcess $V8Path $arguments
$output = $__ib.Output
$exitCode = $__ib.ExitCode
if ($exitCode -ne 0) {
Write-Host "Error loading changes (code: $exitCode)" -ForegroundColor Red
if ($output) { Write-Host ($output | Out-String) }
exit $exitCode
}
Write-Host "Changes loaded successfully ($($configFiles.Count) files)" -ForegroundColor Green
if ($output) { Write-Host ($output | Out-String) }
if ($UpdateDB) {
$applyArgs = @("infobase", "config", "apply", "--db-path=$InfoBasePath", "--force")
if ($UserName) { $applyArgs += "--user=$UserName" }
if ($Password) { $applyArgs += "--password=$Password" }
$applyArgs += "--data=$tempDir"
Write-Host "Running: ibcmd $($applyArgs -join ' ')"
$__ib = Invoke-IbcmdProcess $V8Path $applyArgs
$applyOut = $__ib.Output
$exitCode = $__ib.ExitCode
if ($exitCode -eq 0) {
Write-Host "Database configuration updated successfully" -ForegroundColor Green
} else {
Write-Host "Error updating database configuration (code: $exitCode)" -ForegroundColor Red
}
if ($applyOut) { Write-Host ($applyOut | Out-String) }
}
exit $exitCode
}
# --- 1cv8 branch ---
# --- Write list file (UTF-8 with BOM) --- # --- Write list file (UTF-8 with BOM) ---
$listFile = Join-Path $tempDir "load_list.txt" $listFile = Join-Path $tempDir "load_list.txt"
$utf8Bom = New-Object System.Text.UTF8Encoding($true) $utf8Bom = New-Object System.Text.UTF8Encoding($true)
+109 -17
View File
@@ -1,8 +1,9 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-load-git v1.5 — Load Git changes into 1C database # db-load-git v1.11 — 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
import atexit
import glob import glob
import json import json
import os import os
@@ -35,36 +36,69 @@ def _find_project_v8path():
d = parent d = parent
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
if os.path.basename(parent).lower() == "bin":
parent = os.path.dirname(parent)
return os.path.basename(parent)
def _version_key(p): def _version_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe).""" """Numeric sort key from version dir name."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p))) return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path): def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe.""" """Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path: if not v8path:
v8path = _find_project_v8path() v8path = _find_project_v8path()
if not v8path: if not v8path:
candidates = ( if os.name == "nt":
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe") candidates = (
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
) + glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
else:
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
candidates = glob.glob("/opt/1cv8/*/1cv8")
if candidates: if candidates:
v8path = max(candidates, key=_version_key) v8path = max(candidates, key=_version_key)
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path))) print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
print(f"Auto-selected platform {ver}: {v8path}")
else: else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1) sys.exit(1)
if os.path.isdir(v8path): if os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe") # PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
v8path = os.path.join(v8path, exe)
if not os.path.isfile(v8path): if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr) print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
sys.exit(1) sys.exit(1)
return v8path return v8path
IBCMD_NOUSER_HINT = (
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
"call may block instead of failing. If it does not return promptly, abort and "
"re-run with -UserName and -Password.\n"
)
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
On Windows without -UserName ibcmd reads the console directly and may still block —
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
"""
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
def get_object_xml_from_subfile(relative_path): def get_object_xml_from_subfile(relative_path):
"""Map sub-file path (BSL, HTML, etc.) to object XML path.""" """Map sub-file path (BSL, HTML, etc.) to object XML path."""
parts = re.split(r"[\\/]", relative_path) parts = re.split(r"[\\/]", relative_path)
@@ -76,7 +110,7 @@ def get_object_xml_from_subfile(relative_path):
def run_git(config_dir, git_args): def run_git(config_dir, git_args):
"""Run a git command in config_dir and return output lines on success.""" """Run a git command in config_dir and return output lines on success."""
result = subprocess.run( result = subprocess.run(
["git"] + git_args, ["git", "-c", "core.quotePath=false"] + git_args,
capture_output=True, capture_output=True,
text=True, text=True,
encoding="utf-8", encoding="utf-8",
@@ -125,9 +159,15 @@ def main():
if not args.DryRun: if not args.DryRun:
v8path = resolve_v8path(args.V8Path) v8path = resolve_v8path(args.V8Path)
# --- Validate connection (skip if DryRun) --- # --- Detect engine + validate connection (skip if DryRun) ---
engine = "1cv8"
if not args.DryRun: if not args.DryRun:
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
if engine == "ibcmd":
if not args.InfoBasePath:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
sys.exit(1)
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr) print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1) sys.exit(1)
@@ -248,6 +288,58 @@ def main():
os.makedirs(temp_dir, exist_ok=True) os.makedirs(temp_dir, exist_ok=True)
try: try:
if engine == "ibcmd":
# --- ibcmd branch (file infobase only; import specific files) ---
if args.Format == "Plain":
print("Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)", file=sys.stderr)
sys.exit(1)
if args.AllExtensions:
print("Error: ibcmd config import does not support -AllExtensions (use -Extension or 1cv8)", file=sys.stderr)
sys.exit(1)
arguments = ["infobase", "config", "import", "files"] + config_files
arguments += [f"--base-dir={args.ConfigDir}", f"--db-path={args.InfoBasePath}"]
if args.Extension:
arguments.append(f"--extension={args.Extension}")
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
if args.UserName:
arguments.append(f"--user={args.UserName}")
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(arguments)}")
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
if result.returncode != 0:
print(f"Error loading changes (code: {result.returncode})", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
sys.exit(result.returncode)
print(f"Changes loaded successfully ({len(config_files)} files)")
if result.stdout:
print(result.stdout)
exit_code = 0
if args.UpdateDB:
apply_args = ["infobase", "config", "apply", f"--db-path={args.InfoBasePath}", "--force"]
if args.UserName:
apply_args.append(f"--user={args.UserName}")
if args.Password:
apply_args.append(f"--password={args.Password}")
apply_args.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(apply_args)}")
ar = run_ibcmd([v8path] + apply_args, bool(args.UserName))
exit_code = ar.returncode
if exit_code == 0:
print("Database configuration updated successfully")
else:
print(f"Error updating database configuration (code: {exit_code})", file=sys.stderr)
if ar.stdout:
print(ar.stdout)
if ar.stderr:
print(ar.stderr, file=sys.stderr)
sys.exit(exit_code)
# --- Write list file (UTF-8 with BOM) --- # --- Write list file (UTF-8 with BOM) ---
list_file = os.path.join(temp_dir, "load_list.txt") list_file = os.path.join(temp_dir, "load_list.txt")
with open(list_file, "w", encoding="utf-8-sig") as f: with open(list_file, "w", encoding="utf-8-sig") as f:
+7 -7
View File
@@ -30,7 +30,7 @@ allowed-tools:
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` 2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default` 4. Если ветка не совпала — используй `default`
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если файла нет — предложи `/db-list add`. Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
Если в записи базы указан `configSrc` — используй как каталог загрузки по умолчанию. Если в записи базы указан `configSrc` — используй как каталог загрузки по умолчанию.
@@ -38,14 +38,14 @@ 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" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | | `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Файловая база | | `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | | `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере | | `-InfoBaseRef <имя>` | * | Имя базы на сервере |
@@ -88,14 +88,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,5 +1,6 @@
# db-load-xml v1.5 — Load 1C configuration from XML files # db-load-xml v1.12 — Load 1C configuration from XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
.SYNOPSIS .SYNOPSIS
Загрузка конфигурации 1С из XML-файлов Загрузка конфигурации 1С из XML-файлов
@@ -137,7 +138,7 @@ if (-not $V8Path) {
$V8Path = $found.FullName $V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else { } else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
exit 1 exit 1
} }
} }
@@ -146,12 +147,47 @@ if (Test-Path $V8Path -PathType Container) {
} }
if (-not (Test-Path $V8Path)) { if (-not (Test-Path $V8Path)) {
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
exit 1 exit 1
} }
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
function Invoke-IbcmdProcess {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
param([string]$Exe, [string[]]$IbArgs)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $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.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd()
$err = $p.StandardError.ReadToEnd()
$p.WaitForExit()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
# --- Validate connection --- # --- Validate connection ---
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
exit 1
}
} elseif (-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
exit 1 exit 1
} }
@@ -173,6 +209,73 @@ $tempDir = Join-Path $env:TEMP "db_load_xml_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
try { try {
if ($engine -eq "ibcmd") {
# --- ibcmd branch (file infobase only; hierarchical full-directory import) ---
if ($Format -eq "Plain") {
Write-Host "Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red
exit 1
}
if ($AllExtensions) {
$arguments = @("infobase", "config", "import", "all-extensions", "$ConfigDir", "--db-path=$InfoBasePath")
} elseif ($Mode -eq "Partial" -or $Files -or $ListFile) {
# partial: import specific files (relative to ConfigDir)
$fileList = @()
if ($ListFile) {
if (-not (Test-Path $ListFile)) {
Write-Host "Error: list file not found: $ListFile" -ForegroundColor Red
exit 1
}
$fileList = @(Get-Content -Path $ListFile -Encoding UTF8 | ForEach-Object { $_.Trim() } | Where-Object { $_ })
} elseif ($Files) {
$fileList = @($Files -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
}
if ($fileList.Count -eq 0) {
Write-Host "Error: -Files or -ListFile required for partial import" -ForegroundColor Red
exit 1
}
$arguments = @("infobase", "config", "import", "files") + $fileList
$arguments += "--base-dir=$ConfigDir", "--db-path=$InfoBasePath"
if ($Extension) { $arguments += "--extension=$Extension" }
} else {
$arguments = @("infobase", "config", "import", "--db-path=$InfoBasePath")
if ($Extension) { $arguments += "--extension=$Extension" }
$arguments += "$ConfigDir"
}
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $($arguments -join ' ')"
$__ib = Invoke-IbcmdProcess $V8Path $arguments
$output = $__ib.Output
$exitCode = $__ib.ExitCode
if ($exitCode -ne 0) {
Write-Host "Error loading configuration from files (code: $exitCode)" -ForegroundColor Red
if ($output) { Write-Host ($output | Out-String) }
exit $exitCode
}
Write-Host "Configuration loaded successfully from: $ConfigDir" -ForegroundColor Green
if ($output) { Write-Host ($output | Out-String) }
if ($UpdateDB) {
$applyArgs = @("infobase", "config", "apply", "--db-path=$InfoBasePath", "--force")
if ($UserName) { $applyArgs += "--user=$UserName" }
if ($Password) { $applyArgs += "--password=$Password" }
$applyArgs += "--data=$tempDir"
Write-Host "Running: ibcmd $($applyArgs -join ' ')"
$__ib = Invoke-IbcmdProcess $V8Path $applyArgs
$applyOut = $__ib.Output
$exitCode = $__ib.ExitCode
if ($exitCode -eq 0) {
Write-Host "Database configuration updated successfully" -ForegroundColor Green
} else {
Write-Host "Error updating database configuration (code: $exitCode)" -ForegroundColor Red
}
if ($applyOut) { Write-Host ($applyOut | Out-String) }
}
exit $exitCode
}
# --- 1cv8 branch ---
# --- Build arguments --- # --- Build arguments ---
$arguments = @("DESIGNER") $arguments = @("DESIGNER")
+126 -15
View File
@@ -1,8 +1,9 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-load-xml v1.5 — Load 1C configuration from XML files # db-load-xml v1.12 — 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
import atexit
import glob import glob
import json import json
import os import os
@@ -35,36 +36,69 @@ def _find_project_v8path():
d = parent d = parent
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
if os.path.basename(parent).lower() == "bin":
parent = os.path.dirname(parent)
return os.path.basename(parent)
def _version_key(p): def _version_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe).""" """Numeric sort key from version dir name."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p))) return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path): def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe.""" """Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path: if not v8path:
v8path = _find_project_v8path() v8path = _find_project_v8path()
if not v8path: if not v8path:
candidates = ( if os.name == "nt":
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe") candidates = (
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
) + glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
else:
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
candidates = glob.glob("/opt/1cv8/*/1cv8")
if candidates: if candidates:
v8path = max(candidates, key=_version_key) v8path = max(candidates, key=_version_key)
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path))) print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
print(f"Auto-selected platform {ver}: {v8path}")
else: else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1) sys.exit(1)
if os.path.isdir(v8path): if os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe") # PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
v8path = os.path.join(v8path, exe)
if not os.path.isfile(v8path): if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr) print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
sys.exit(1) sys.exit(1)
return v8path return v8path
IBCMD_NOUSER_HINT = (
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
"call may block instead of failing. If it does not return promptly, abort and "
"re-run with -UserName and -Password.\n"
)
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
On Windows without -UserName ibcmd reads the console directly and may still block
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
"""
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
def main(): def main():
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
@@ -106,8 +140,14 @@ def main():
# --- 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"
# --- Validate connection --- # --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): if engine == "ibcmd":
if not args.InfoBasePath:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
sys.exit(1)
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr) print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1) sys.exit(1)
@@ -121,6 +161,77 @@ def main():
print("Error: -Files or -ListFile required for Partial mode", file=sys.stderr) print("Error: -Files or -ListFile required for Partial mode", file=sys.stderr)
sys.exit(1) sys.exit(1)
# --- ibcmd branch (file infobase only; hierarchical full-directory import) ---
if engine == "ibcmd":
if args.Format == "Plain":
print("Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)", file=sys.stderr)
sys.exit(1)
if args.AllExtensions:
arguments = ["infobase", "config", "import", "all-extensions", args.ConfigDir, f"--db-path={args.InfoBasePath}"]
elif args.Mode == "Partial" or args.Files or args.ListFile:
# partial: import specific files (relative to ConfigDir)
if args.ListFile:
if not os.path.isfile(args.ListFile):
print(f"Error: list file not found: {args.ListFile}", file=sys.stderr)
sys.exit(1)
with open(args.ListFile, encoding="utf-8-sig") as f:
file_list = [ln.strip() for ln in f if ln.strip()]
elif args.Files:
file_list = [p.strip() for p in args.Files.split(",") if p.strip()]
else:
file_list = []
if not file_list:
print("Error: -Files or -ListFile required for partial import", file=sys.stderr)
sys.exit(1)
arguments = ["infobase", "config", "import", "files"] + file_list
arguments += [f"--base-dir={args.ConfigDir}", f"--db-path={args.InfoBasePath}"]
if args.Extension:
arguments.append(f"--extension={args.Extension}")
else:
arguments = ["infobase", "config", "import", f"--db-path={args.InfoBasePath}"]
if args.Extension:
arguments.append(f"--extension={args.Extension}")
arguments.append(args.ConfigDir)
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
if args.UserName:
arguments.append(f"--user={args.UserName}")
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(arguments)}")
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
if result.returncode != 0:
print(f"Error loading configuration from files (code: {result.returncode})", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
sys.exit(result.returncode)
print(f"Configuration loaded successfully from: {args.ConfigDir}")
if result.stdout:
print(result.stdout)
exit_code = 0
if args.UpdateDB:
apply_args = ["infobase", "config", "apply", f"--db-path={args.InfoBasePath}", "--force"]
if args.UserName:
apply_args.append(f"--user={args.UserName}")
if args.Password:
apply_args.append(f"--password={args.Password}")
apply_args.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(apply_args)}")
ar = run_ibcmd([v8path] + apply_args, bool(args.UserName))
exit_code = ar.returncode
if exit_code == 0:
print("Database configuration updated successfully")
else:
print(f"Error updating database configuration (code: {exit_code})", file=sys.stderr)
if ar.stdout:
print(ar.stdout)
if ar.stderr:
print(ar.stderr, file=sys.stderr)
sys.exit(exit_code)
# --- Temp dir --- # --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_xml_{random.randint(0, 999999)}") temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_xml_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True) os.makedirs(temp_dir, exist_ok=True)
+6 -6
View File
@@ -29,14 +29,14 @@ allowed-tools:
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` 2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default` 4. Если ветка не совпала — используй `default`
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если файла нет — предложи `/db-list add`. Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" <параметры> python "${CLAUDE_SKILL_DIR}/scripts/db-run.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -63,14 +63,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 "ЗапуститьОбновление"
``` ```
+4 -3
View File
@@ -1,5 +1,6 @@
# db-run v1.1 — Launch 1C:Enterprise # db-run v1.2 — Launch 1C:Enterprise
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
.SYNOPSIS .SYNOPSIS
Запуск 1С:Предприятие Запуск 1С:Предприятие
@@ -108,7 +109,7 @@ if (-not $V8Path) {
$V8Path = $found.FullName $V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else { } else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
exit 1 exit 1
} }
} }
@@ -117,7 +118,7 @@ if (Test-Path $V8Path -PathType Container) {
} }
if (-not (Test-Path $V8Path)) { if (-not (Test-Path $V8Path)) {
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
exit 1 exit 1
} }
+26 -14
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-run v1.1 — Launch 1C:Enterprise # db-run v1.2 — 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
@@ -32,32 +32,44 @@ def _find_project_v8path():
d = parent d = parent
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
if os.path.basename(parent).lower() == "bin":
parent = os.path.dirname(parent)
return os.path.basename(parent)
def _version_key(p): def _version_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe).""" """Numeric sort key from version dir name."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p))) return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path): def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe.""" """Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path: if not v8path:
v8path = _find_project_v8path() v8path = _find_project_v8path()
if not v8path: if not v8path:
candidates = ( if os.name == "nt":
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe") candidates = (
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
) + glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
else:
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
candidates = glob.glob("/opt/1cv8/*/1cv8")
if candidates: if candidates:
v8path = max(candidates, key=_version_key) v8path = max(candidates, key=_version_key)
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path))) print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
print(f"Auto-selected platform {ver}: {v8path}")
else: else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1) sys.exit(1)
if os.path.isdir(v8path): if os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe") # PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
v8path = os.path.join(v8path, exe)
if not os.path.isfile(v8path): if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr) print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
sys.exit(1) sys.exit(1)
return v8path return v8path
+6 -6
View File
@@ -28,21 +28,21 @@ allowed-tools:
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` 2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default` 4. Если ветка не совпала — используй `default`
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если файла нет — предложи `/db-list add`. Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" <параметры> python "${CLAUDE_SKILL_DIR}/scripts/db-update.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | | `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Файловая база | | `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | | `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере | | `-InfoBaseRef <имя>` | * | Имя базы на сервере |
@@ -76,11 +76,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 "МоёРасширение"
``` ```
+67 -4
View File
@@ -1,5 +1,6 @@
# db-update v1.1 — Update 1C database configuration # db-update v1.6 — Update 1C database configuration
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
.SYNOPSIS .SYNOPSIS
Обновление конфигурации базы данных 1С Обновление конфигурации базы данных 1С
@@ -118,7 +119,7 @@ if (-not $V8Path) {
$V8Path = $found.FullName $V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else { } else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
exit 1 exit 1
} }
} }
@@ -127,12 +128,47 @@ if (Test-Path $V8Path -PathType Container) {
} }
if (-not (Test-Path $V8Path)) { if (-not (Test-Path $V8Path)) {
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
exit 1 exit 1
} }
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
function Invoke-IbcmdProcess {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
param([string]$Exe, [string[]]$IbArgs)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $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.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd()
$err = $p.StandardError.ReadToEnd()
$p.WaitForExit()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
# --- Validate connection --- # --- Validate connection ---
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
exit 1
}
} elseif (-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
exit 1 exit 1
} }
@@ -142,6 +178,33 @@ $tempDir = Join-Path $env:TEMP "db_update_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
try { try {
if ($engine -eq "ibcmd") {
# --- ibcmd branch (file infobase only) ---
if ($AllExtensions) {
Write-Host "Error: ibcmd config apply does not support -AllExtensions (use -Extension)" -ForegroundColor Red
exit 1
}
$arguments = @("infobase", "config", "apply", "--db-path=$InfoBasePath", "--force")
if ($Dynamic -eq "+") { $arguments += "--dynamic=auto" }
elseif ($Dynamic -eq "-") { $arguments += "--dynamic=disable" }
if ($Extension) { $arguments += "--extension=$Extension" }
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $($arguments -join ' ')"
$__ib = Invoke-IbcmdProcess $V8Path $arguments
$output = $__ib.Output
$exitCode = $__ib.ExitCode
if ($exitCode -eq 0) {
Write-Host "Database configuration updated successfully" -ForegroundColor Green
} else {
Write-Host "Error updating database configuration (code: $exitCode)" -ForegroundColor Red
}
if ($output) { Write-Host ($output | Out-String) }
exit $exitCode
}
# --- 1cv8 branch ---
# --- Build arguments --- # --- Build arguments ---
$arguments = @("DESIGNER") $arguments = @("DESIGNER")
+86 -15
View File
@@ -1,8 +1,9 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-update v1.1 — Update 1C database configuration # db-update v1.6 — 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
import atexit
import glob import glob
import json import json
import os import os
@@ -35,36 +36,69 @@ def _find_project_v8path():
d = parent d = parent
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
if os.path.basename(parent).lower() == "bin":
parent = os.path.dirname(parent)
return os.path.basename(parent)
def _version_key(p): def _version_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe).""" """Numeric sort key from version dir name."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p))) return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path): def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe.""" """Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path: if not v8path:
v8path = _find_project_v8path() v8path = _find_project_v8path()
if not v8path: if not v8path:
candidates = ( if os.name == "nt":
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe") candidates = (
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
) + glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
else:
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
candidates = glob.glob("/opt/1cv8/*/1cv8")
if candidates: if candidates:
v8path = max(candidates, key=_version_key) v8path = max(candidates, key=_version_key)
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path))) print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
print(f"Auto-selected platform {ver}: {v8path}")
else: else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1) sys.exit(1)
if os.path.isdir(v8path): if os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe") # PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
v8path = os.path.join(v8path, exe)
if not os.path.isfile(v8path): if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr) print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
sys.exit(1) sys.exit(1)
return v8path return v8path
IBCMD_NOUSER_HINT = (
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
"call may block instead of failing. If it does not return promptly, abort and "
"re-run with -UserName and -Password.\n"
)
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
On Windows without -UserName ibcmd reads the console directly and may still block
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
"""
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
def main(): def main():
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
@@ -87,11 +121,48 @@ def main():
v8path = resolve_v8path(args.V8Path) v8path = resolve_v8path(args.V8Path)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Validate connection --- # --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): if engine == "ibcmd":
if not args.InfoBasePath:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
sys.exit(1)
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr) print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1) sys.exit(1)
# --- ibcmd branch (file infobase only) ---
if engine == "ibcmd":
if args.AllExtensions:
print("Error: ibcmd config apply does not support -AllExtensions (use -Extension)", file=sys.stderr)
sys.exit(1)
arguments = ["infobase", "config", "apply", f"--db-path={args.InfoBasePath}", "--force"]
if args.Dynamic == "+":
arguments.append("--dynamic=auto")
elif args.Dynamic == "-":
arguments.append("--dynamic=disable")
if args.Extension:
arguments.append(f"--extension={args.Extension}")
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
if args.UserName:
arguments.append(f"--user={args.UserName}")
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(arguments)}")
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
if result.returncode == 0:
print("Database configuration updated successfully")
else:
print(f"Error updating database configuration (code: {result.returncode})", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
sys.exit(result.returncode)
# --- Temp dir --- # --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_update_{random.randint(0, 999999)}") temp_dir = os.path.join(tempfile.gettempdir(), f"db_update_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True) os.makedirs(temp_dir, exist_ok=True)
+5 -5
View File
@@ -34,20 +34,20 @@ allowed-tools:
5. Если ветка не совпала — используй `default` 5. Если ветка не совпала — используй `default`
6. Если `.v8-project.json` нет или база не найдена — не указывай параметры подключения: скрипт автоматически создаст временную базу. Для EPF со ссылочными типами (CatalogRef, DocumentRef и т.д.) генерируются заглушки метаданных. Временная база удаляется после сборки. 6. Если `.v8-project.json` нет или база не найдена — не указывай параметры подключения: скрипт автоматически создаст временную базу. Для EPF со ссылочными типами (CatalogRef, DocumentRef и т.д.) генерируются заглушки метаданных. Временная база удаляется после сборки.
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" <параметры> python "${CLAUDE_SKILL_DIR}/scripts/epf-build.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | | `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Файловая база | | `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | | `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере | | `-InfoBaseRef <имя>` | * | Имя базы на сервере |
@@ -62,8 +62,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"
``` ```
+59 -3
View File
@@ -1,5 +1,6 @@
# epf-build v1.1 — Build external data processor or report (EPF/ERF) from XML sources # epf-build v1.6 — Build external data processor or report (EPF/ERF) from XML sources
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
.SYNOPSIS .SYNOPSIS
Сборка внешней обработки/отчёта 1С из XML-исходников Сборка внешней обработки/отчёта 1С из XML-исходников
@@ -99,7 +100,7 @@ if (-not $V8Path) {
$V8Path = $found.FullName $V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else { } else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
exit 1 exit 1
} }
} }
@@ -108,7 +109,41 @@ if (Test-Path $V8Path -PathType Container) {
} }
if (-not (Test-Path $V8Path)) { if (-not (Test-Path $V8Path)) {
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
exit 1
}
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
function Invoke-IbcmdProcess {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
param([string]$Exe, [string[]]$IbArgs)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $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.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd()
$err = $p.StandardError.ReadToEnd()
$p.WaitForExit()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
if ($engine -eq "ibcmd" -and $InfoBaseServer -and $InfoBaseRef) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath or omit for stub)" -ForegroundColor Red
exit 1 exit 1
} }
@@ -146,6 +181,27 @@ $tempDir = Join-Path $env:TEMP "epf_build_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
try { try {
if ($engine -eq "ibcmd") {
# --- ibcmd branch: build EPF/ERF via config import --out ---
$srcDir = Split-Path $SourceFile -Parent
$arguments = @("infobase", "config", "import", "$srcDir", "--out=$OutputFile", "--db-path=$InfoBasePath")
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $($arguments -join ' ')"
$__ib = Invoke-IbcmdProcess $V8Path $arguments
$output = $__ib.Output
$exitCode = $__ib.ExitCode
if ($exitCode -eq 0) {
Write-Host "External data processor/report built successfully: $OutputFile" -ForegroundColor Green
} else {
Write-Host "Error building external data processor/report (code: $exitCode)" -ForegroundColor Red
}
if ($output) { Write-Host ($output | Out-String) }
exit $exitCode
}
# --- 1cv8 branch ---
# --- Build arguments --- # --- Build arguments ---
$arguments = @("DESIGNER") $arguments = @("DESIGNER")
+75 -14
View File
@@ -1,8 +1,9 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# epf-build v1.1 — Build external data processor or report (EPF/ERF) from XML sources # epf-build v1.6 — 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
import atexit
import glob import glob
import json import json
import os import os
@@ -35,36 +36,69 @@ def _find_project_v8path():
d = parent d = parent
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
if os.path.basename(parent).lower() == "bin":
parent = os.path.dirname(parent)
return os.path.basename(parent)
def _version_key(p): def _version_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe).""" """Numeric sort key from version dir name."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p))) return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path): def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe.""" """Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path: if not v8path:
v8path = _find_project_v8path() v8path = _find_project_v8path()
if not v8path: if not v8path:
candidates = ( if os.name == "nt":
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe") candidates = (
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
) + glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
else:
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
candidates = glob.glob("/opt/1cv8/*/1cv8")
if candidates: if candidates:
v8path = max(candidates, key=_version_key) v8path = max(candidates, key=_version_key)
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path))) print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
print(f"Auto-selected platform {ver}: {v8path}")
else: else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1) sys.exit(1)
if os.path.isdir(v8path): if os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe") # PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
v8path = os.path.join(v8path, exe)
if not os.path.isfile(v8path): if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr) print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
sys.exit(1) sys.exit(1)
return v8path return v8path
IBCMD_NOUSER_HINT = (
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
"call may block instead of failing. If it does not return promptly, abort and "
"re-run with -UserName and -Password.\n"
)
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
On Windows without -UserName ibcmd reads the console directly and may still block
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
"""
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
def main(): def main():
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
@@ -84,6 +118,10 @@ def main():
# --- 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"
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)
sys.exit(1)
# --- Auto-create stub database if no connection specified --- # --- Auto-create stub database if no connection specified ---
auto_created_base = None auto_created_base = None
@@ -117,6 +155,29 @@ def main():
os.makedirs(temp_dir, exist_ok=True) os.makedirs(temp_dir, exist_ok=True)
try: try:
if engine == "ibcmd":
# --- ibcmd branch: build EPF/ERF via config import --out ---
src_dir = os.path.dirname(os.path.abspath(args.SourceFile))
arguments = ["infobase", "config", "import", src_dir, f"--out={args.OutputFile}", f"--db-path={args.InfoBasePath}"]
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
if args.UserName:
arguments.append(f"--user={args.UserName}")
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(arguments)}")
result = run_ibcmd([v8path] + arguments, warn_no_user=False)
if result.returncode == 0:
print(f"External data processor/report built successfully: {args.OutputFile}")
else:
print(f"Error building external data processor/report (code: {result.returncode})", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
sys.exit(result.returncode)
# --- Build arguments --- # --- Build arguments ---
arguments = ["DESIGNER"] arguments = ["DESIGNER"]
@@ -1,4 +1,4 @@
# stub-db-create v1.0 — Create temp 1C infobase with metadata stubs for EPF/ERF build # stub-db-create v1.3 — 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)]
@@ -1252,6 +1252,57 @@ $propsXml </Properties>$childObjLine
} }
} }
# --- 5a. Stub via ibcmd (one call: create [--import --apply]) ---
function Invoke-IbcmdProcess {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
param([string]$Exe, [string[]]$IbArgs)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $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.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd()
$err = $p.StandardError.ReadToEnd()
$p.WaitForExit()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
$stubEngine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
if ($stubEngine -eq "ibcmd") {
Write-Host "Creating infobase (ibcmd): $TempBasePath"
$ibData = Join-Path $env:TEMP "stub_data_$(Get-Random)"
New-Item -ItemType Directory -Path $ibData -Force | Out-Null
$ibArgs = @("infobase", "create", "--db-path=$TempBasePath", "--create-database")
if ($hasRefTypes) { $ibArgs += "--import=$(Join-Path $TempBasePath 'cfg')", "--apply", "--force" }
$ibArgs += "--data=$ibData"
$__ib = Invoke-IbcmdProcess $V8Path $ibArgs
$ibOut = $__ib.Output
$ibRc = $__ib.ExitCode
Remove-Item -Path $ibData -Recurse -Force -ErrorAction SilentlyContinue
if ($ibRc -ne 0) {
if ($ibOut) { Write-Host ($ibOut | Out-String) }
Write-Error "Failed to create stub infobase (code: $ibRc)"
exit 1
}
if ($hasRefTypes) { Remove-Item -Path (Join-Path $TempBasePath "cfg") -Recurse -Force -ErrorAction SilentlyContinue }
Write-Host "[OK] Stub database created: $TempBasePath"
Write-Host $TempBasePath
exit 0
}
# --- 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"
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# stub-db-create v1.0 — Create temp 1C infobase with metadata stubs for EPF/ERF build # stub-db-create v1.3 — Create temp 1C infobase with metadata stubs for EPF/ERF build
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -12,6 +12,27 @@ import tempfile
import uuid import uuid
IBCMD_NOUSER_HINT = (
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
"call may block instead of failing. If it does not return promptly, abort and "
"re-run with -UserName and -Password.\n"
)
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
On Windows without -UserName ibcmd reads the console directly and may still block
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
"""
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
def new_uuid(): def new_uuid():
return str(uuid.uuid4()) return str(uuid.uuid4())
@@ -1034,6 +1055,32 @@ def main():
if register_columns: if register_columns:
print('WARNING: Register column categories (Dimension/Resource/Attribute) are guessed. Form field bindings may not survive round-trip through a real database.') print('WARNING: Register column categories (Dimension/Resource/Attribute) are guessed. Form field bindings may not survive round-trip through a real database.')
# Stub via ibcmd (one call: create [--import --apply])
stub_engine = "ibcmd" if os.path.basename(args.V8Path).lower().startswith("ibcmd") else "1cv8"
if stub_engine == "ibcmd":
import shutil
print(f'Creating infobase (ibcmd): {temp_base}')
ib_data = tempfile.mkdtemp(prefix="stub_data_")
ib_args = [args.V8Path, 'infobase', 'create', f'--db-path={temp_base}', '--create-database']
if has_ref_types:
ib_args += [f'--import={os.path.join(temp_base, "cfg")}', '--apply', '--force']
ib_args.append(f'--data={ib_data}')
result = run_ibcmd(ib_args, warn_no_user=False)
shutil.rmtree(ib_data, ignore_errors=True)
if result.returncode != 0:
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
print(f'Failed to create stub infobase (code: {result.returncode})', file=sys.stderr)
sys.exit(1)
if has_ref_types:
import shutil
shutil.rmtree(os.path.join(temp_base, 'cfg'), ignore_errors=True)
print(f'[OK] Stub database created: {temp_base}')
print(temp_base)
sys.exit(0)
# Create infobase # Create infobase
print(f'Creating infobase: {temp_base}') print(f'Creating infobase: {temp_base}')
result = subprocess.run( result = subprocess.run(
+5 -5
View File
@@ -33,20 +33,20 @@ allowed-tools:
5. Если ветка не совпала — используй `default` 5. Если ветка не совпала — используй `default`
6. Если `.v8-project.json` нет или база не найдена — **сообщи пользователю об ошибке**. Для dump база обязательна: в пустой базе ссылочные типы (CatalogRef, DocumentRef и т.д.) безвозвратно сбрасываются в строки. Предложи указать базу или зарегистрировать через `/db-list add`. 6. Если `.v8-project.json` нет или база не найдена — **сообщи пользователю об ошибке**. Для dump база обязательна: в пустой базе ссылочные типы (CatalogRef, DocumentRef и т.д.) безвозвратно сбрасываются в строки. Предложи указать базу или зарегистрировать через `/db-list add`.
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" <параметры> python "${CLAUDE_SKILL_DIR}/scripts/epf-dump.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | | `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Файловая база | | `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | | `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере | | `-InfoBaseRef <имя>` | * | Имя базы на сервере |
@@ -62,8 +62,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"
``` ```
+64 -3
View File
@@ -1,5 +1,6 @@
# epf-dump v1.1 — Dump external data processor or report (EPF/ERF) to XML sources # epf-dump v1.6 — Dump external data processor or report (EPF/ERF) to XML sources
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
.SYNOPSIS .SYNOPSIS
Разборка внешней обработки/отчёта 1С в XML-исходники Разборка внешней обработки/отчёта 1С в XML-исходники
@@ -106,7 +107,7 @@ if (-not $V8Path) {
$V8Path = $found.FullName $V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else { } else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
exit 1 exit 1
} }
} }
@@ -115,7 +116,7 @@ if (Test-Path $V8Path -PathType Container) {
} }
if (-not (Test-Path $V8Path)) { if (-not (Test-Path $V8Path)) {
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
exit 1 exit 1
} }
@@ -126,6 +127,46 @@ if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
exit 1 exit 1
} }
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
function Invoke-IbcmdProcess {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
param([string]$Exe, [string[]]$IbArgs)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $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.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd()
$err = $p.StandardError.ReadToEnd()
$p.WaitForExit()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
exit 1
}
if ($Format -eq "Plain") {
Write-Host "Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red
exit 1
}
}
# --- Validate input file --- # --- Validate input file ---
if (-not (Test-Path $InputFile)) { if (-not (Test-Path $InputFile)) {
Write-Host "Error: input file not found: $InputFile" -ForegroundColor Red Write-Host "Error: input file not found: $InputFile" -ForegroundColor Red
@@ -142,6 +183,26 @@ $tempDir = Join-Path $env:TEMP "epf_dump_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
try { try {
if ($engine -eq "ibcmd") {
# --- ibcmd branch: dump EPF/ERF via config export --file ---
$arguments = @("infobase", "config", "export", "--file=$InputFile", "$OutputDir", "--db-path=$InfoBasePath")
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $($arguments -join ' ')"
$__ib = Invoke-IbcmdProcess $V8Path $arguments
$output = $__ib.Output
$exitCode = $__ib.ExitCode
if ($exitCode -eq 0) {
Write-Host "External data processor/report dumped successfully to: $OutputDir" -ForegroundColor Green
} else {
Write-Host "Error dumping external data processor/report (code: $exitCode)" -ForegroundColor Red
}
if ($output) { Write-Host ($output | Out-String) }
exit $exitCode
}
# --- 1cv8 branch ---
# --- Build arguments --- # --- Build arguments ---
$arguments = @("DESIGNER") $arguments = @("DESIGNER")
+78 -14
View File
@@ -1,8 +1,9 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# epf-dump v1.1 — Dump external data processor or report (EPF/ERF) to XML sources # epf-dump v1.6 — 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
import atexit
import glob import glob
import json import json
import os import os
@@ -35,36 +36,69 @@ def _find_project_v8path():
d = parent d = parent
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
if os.path.basename(parent).lower() == "bin":
parent = os.path.dirname(parent)
return os.path.basename(parent)
def _version_key(p): def _version_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe).""" """Numeric sort key from version dir name."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p))) return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path): def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe.""" """Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path: if not v8path:
v8path = _find_project_v8path() v8path = _find_project_v8path()
if not v8path: if not v8path:
candidates = ( if os.name == "nt":
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe") candidates = (
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
) + glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
else:
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
candidates = glob.glob("/opt/1cv8/*/1cv8")
if candidates: if candidates:
v8path = max(candidates, key=_version_key) v8path = max(candidates, key=_version_key)
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path))) print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
print(f"Auto-selected platform {ver}: {v8path}")
else: else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1) sys.exit(1)
if os.path.isdir(v8path): if os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe") # PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
v8path = os.path.join(v8path, exe)
if not os.path.isfile(v8path): if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr) print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
sys.exit(1) sys.exit(1)
return v8path return v8path
IBCMD_NOUSER_HINT = (
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
"call may block instead of failing. If it does not return promptly, abort and "
"re-run with -UserName and -Password.\n"
)
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
On Windows without -UserName ibcmd reads the console directly and may still block
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
"""
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
def main(): def main():
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
@@ -90,12 +124,20 @@ def main():
# --- 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"
# --- 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)
print("Dump in an empty database loses reference types (CatalogRef, DocumentRef, etc.) irreversibly.") print("Dump in an empty database loses reference types (CatalogRef, DocumentRef, etc.) irreversibly.")
sys.exit(1) sys.exit(1)
if engine == "ibcmd":
if not args.InfoBasePath:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
sys.exit(1)
if args.Format == "Plain":
print("Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)", file=sys.stderr)
sys.exit(1)
# --- Validate input file --- # --- Validate input file ---
if not os.path.isfile(args.InputFile): if not os.path.isfile(args.InputFile):
@@ -111,6 +153,28 @@ def main():
os.makedirs(temp_dir, exist_ok=True) os.makedirs(temp_dir, exist_ok=True)
try: try:
if engine == "ibcmd":
# --- ibcmd branch: dump EPF/ERF via config export --file ---
arguments = ["infobase", "config", "export", f"--file={args.InputFile}", args.OutputDir, f"--db-path={args.InfoBasePath}"]
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
if args.UserName:
arguments.append(f"--user={args.UserName}")
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(arguments)}")
result = run_ibcmd([v8path] + arguments, warn_no_user=False)
if result.returncode == 0:
print(f"External data processor/report dumped successfully to: {args.OutputDir}")
else:
print(f"Error dumping external data processor/report (code: {result.returncode})", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
sys.exit(result.returncode)
# --- Build arguments --- # --- Build arguments ---
arguments = ["DESIGNER"] arguments = ["DESIGNER"]
+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"
``` ```
+4 -4
View File
@@ -34,7 +34,7 @@ allowed-tools:
5. Если ветка не совпала — используй `default` 5. Если ветка не совпала — используй `default`
6. Если `.v8-project.json` нет или база не найдена — не указывай параметры подключения: скрипт автоматически создаст временную базу. Для ERF со ссылочными типами (CatalogRef, DocumentRef и т.д.) генерируются заглушки метаданных. Временная база удаляется после сборки. 6. Если `.v8-project.json` нет или база не найдена — не указывай параметры подключения: скрипт автоматически создаст временную базу. Для ERF со ссылочными типами (CatalogRef, DocumentRef и т.д.) генерируются заглушки метаданных. Временная база удаляется после сборки.
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда ## Команда
@@ -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" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -64,8 +64,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"
``` ```
+4 -4
View File
@@ -33,7 +33,7 @@ allowed-tools:
5. Если ветка не совпала — используй `default` 5. Если ветка не совпала — используй `default`
6. Если `.v8-project.json` нет или база не найдена — **сообщи пользователю об ошибке**. Для dump база обязательна: в пустой базе ссылочные типы (CatalogRef, DocumentRef и т.д.) безвозвратно сбрасываются в строки. Предложи указать базу или зарегистрировать через `/db-list add`. 6. Если `.v8-project.json` нет или база не найдена — **сообщи пользователю об ошибке**. Для dump база обязательна: в пустой базе ссылочные типы (CatalogRef, DocumentRef и т.д.) безвозвратно сбрасываются в строки. Предложи указать базу или зарегистрировать через `/db-list add`.
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда ## Команда
@@ -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" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -64,8 +64,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 -2
View File
@@ -1,4 +1,4 @@
# form-add v1.7 — Add managed form to 1C config object # form-add v1.9 — Add managed form to 1C config object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -33,6 +33,16 @@ function Get-RootUuid([string]$xmlPath) {
} catch {} } catch {}
return $null return $null
} }
function Test-ExternalObjectRoot([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $false }
try {
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
$el = $mx.DocumentElement.FirstChild
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
} catch {}
return $false
}
function Find-V8Project([string]$startDir) { function Find-V8Project([string]$startDir) {
$d = $startDir $d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) { for ($i = 0; $i -lt 20 -and $d; $i++) {
@@ -69,10 +79,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
try { try {
$rp = $targetPath $rp = $targetPath
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {} try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
if (Test-ExternalObjectRoot $rp) { return }
$elemUuid = Get-RootUuid $rp $elemUuid = Get-RootUuid $rp
$cfgDir = $null; $binPath = $null $cfgDir = $null; $binPath = $null
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) } $d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
for ($i = 0; $i -lt 12 -and $d; $i++) { for ($i = 0; $i -lt 12 -and $d; $i++) {
if (Test-ExternalObjectRoot "$d.xml") { return }
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" } if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $cfgDir) { if (-not $cfgDir) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
@@ -195,7 +208,7 @@ $supportedTypes = @(
"Document", "Catalog", "DataProcessor", "Report", "Document", "Catalog", "DataProcessor", "Report",
"ExternalDataProcessor", "ExternalReport", "ExternalDataProcessor", "ExternalReport",
"InformationRegister", "AccumulationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes", "InformationRegister", "AccumulationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
"ExchangePlan", "BusinessProcess", "Task" "ExchangePlan", "BusinessProcess", "Task", "DocumentJournal"
) )
$objectType = $null $objectType = $null
+19 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# form-add v1.7 — Add managed form to 1C config object # form-add v1.9 — Add managed form to 1C config object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -32,6 +32,18 @@ def _sg_root_uuid(xml_path):
return None return None
def _sg_is_external_root(xml_path):
if not os.path.isfile(xml_path):
return False
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str):
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
except Exception:
return False
return False
def _sg_find_v8project(start_dir): def _sg_find_v8project(start_dir):
d = start_dir d = start_dir
for _ in range(20): for _ in range(20):
@@ -71,6 +83,9 @@ def _sg_get_edit_mode(cfg_dir):
def assert_edit_allowed(target_path, require): def assert_edit_allowed(target_path, require):
try: try:
rp = os.path.abspath(target_path) rp = os.path.abspath(target_path)
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
if _sg_is_external_root(rp):
return
elem_uuid = _sg_root_uuid(rp) elem_uuid = _sg_root_uuid(rp)
cfg_dir = None cfg_dir = None
bin_path = None bin_path = None
@@ -78,6 +93,8 @@ def assert_edit_allowed(target_path, require):
for _ in range(12): for _ in range(12):
if not d: if not d:
break break
if _sg_is_external_root(d + ".xml"):
return
if not elem_uuid: if not elem_uuid:
elem_uuid = _sg_root_uuid(d + ".xml") elem_uuid = _sg_root_uuid(d + ".xml")
if not cfg_dir: if not cfg_dir:
@@ -256,7 +273,7 @@ def main():
"Document", "Catalog", "DataProcessor", "Report", "Document", "Catalog", "DataProcessor", "Report",
"ExternalDataProcessor", "ExternalReport", "ExternalDataProcessor", "ExternalReport",
"InformationRegister", "AccumulationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes", "InformationRegister", "AccumulationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
"ExchangePlan", "BusinessProcess", "Task", "ExchangePlan", "BusinessProcess", "Task", "DocumentJournal",
] ]
object_type = None object_type = None
+3 -2
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 — справка
@@ -187,6 +187,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-compile.ps1" -
| `showTitle: true` | Показывать заголовок группы | | `showTitle: true` | Показывать заголовок группы |
| `united: false` | Левый край полей ввода выравнивается только в пределах этой группы (по умолчанию `true` — сквозное выравнивание по самому длинному заголовку, в т.ч. с соседними группами) | | `united: false` | Левый край полей ввода выравнивается только в пределах этой группы (по умолчанию `true` — сквозное выравнивание по самому длинному заголовку, в т.ч. с соседними группами) |
| `collapsed: true` | Для `behavior: "collapsible"` / `"popup"` — группа создаётся свёрнутой | | `collapsed: true` | Для `behavior: "collapsible"` / `"popup"` — группа создаётся свёрнутой |
| `controlRepresentation` | Отображение управления свёрткой (`behavior: "collapsible"`): `"TitleHyperlink"` (гиперссылка заголовка, по умолчанию) / `"Picture"` (картинка) |
| `representation` | `"none"`, `"normal"`, `"weak"`, `"strong"` | | `representation` | `"none"`, `"normal"`, `"weak"`, `"strong"` |
| `children: [...]` | Вложенные элементы | | `children: [...]` | Вложенные элементы |
@@ -1,4 +1,4 @@
# form-compile v1.174 — Compile 1C managed form from JSON or object metadata # form-compile v1.175 — 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,
@@ -1362,6 +1362,16 @@ function Get-RootUuid([string]$xmlPath) {
} catch {} } catch {}
return $null return $null
} }
function Test-ExternalObjectRoot([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $false }
try {
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
$el = $mx.DocumentElement.FirstChild
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
} catch {}
return $false
}
function Find-V8Project([string]$startDir) { function Find-V8Project([string]$startDir) {
$d = $startDir $d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) { for ($i = 0; $i -lt 20 -and $d; $i++) {
@@ -1398,10 +1408,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
try { try {
$rp = $targetPath $rp = $targetPath
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {} try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
if (Test-ExternalObjectRoot $rp) { return }
$elemUuid = Get-RootUuid $rp $elemUuid = Get-RootUuid $rp
$cfgDir = $null; $binPath = $null $cfgDir = $null; $binPath = $null
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) } $d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
for ($i = 0; $i -lt 12 -and $d; $i++) { for ($i = 0; $i -lt 12 -and $d; $i++) {
if (Test-ExternalObjectRoot "$d.xml") { return }
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" } if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $cfgDir) { if (-not $cfgDir) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# form-compile v1.174 — Compile 1C managed form from JSON or object metadata # form-compile v1.175 — Compile 1C managed form from JSON or object metadata
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
import copy import copy
@@ -34,6 +34,18 @@ def _sg_root_uuid(xml_path):
return None return None
def _sg_is_external_root(xml_path):
if not os.path.isfile(xml_path):
return False
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str):
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
except Exception:
return False
return False
def _sg_find_v8project(start_dir): def _sg_find_v8project(start_dir):
d = start_dir d = start_dir
for _ in range(20): for _ in range(20):
@@ -73,6 +85,9 @@ def _sg_get_edit_mode(cfg_dir):
def assert_edit_allowed(target_path, require): def assert_edit_allowed(target_path, require):
try: try:
rp = os.path.abspath(target_path) rp = os.path.abspath(target_path)
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
if _sg_is_external_root(rp):
return
elem_uuid = _sg_root_uuid(rp) elem_uuid = _sg_root_uuid(rp)
cfg_dir = None cfg_dir = None
bin_path = None bin_path = None
@@ -80,6 +95,8 @@ def assert_edit_allowed(target_path, require):
for _ in range(12): for _ in range(12):
if not d: if not d:
break break
if _sg_is_external_root(d + ".xml"):
return
if not elem_uuid: if not elem_uuid:
elem_uuid = _sg_root_uuid(d + ".xml") elem_uuid = _sg_root_uuid(d + ".xml")
if not cfg_dir: if not cfg_dir:
+1 -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 формат
+14 -1
View File
@@ -1,4 +1,4 @@
# form-edit v1.3 — Edit 1C managed form elements # form-edit v1.4 — Edit 1C managed form elements
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -27,6 +27,16 @@ function Get-RootUuid([string]$xmlPath) {
} catch {} } catch {}
return $null return $null
} }
function Test-ExternalObjectRoot([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $false }
try {
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
$el = $mx.DocumentElement.FirstChild
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
} catch {}
return $false
}
function Find-V8Project([string]$startDir) { function Find-V8Project([string]$startDir) {
$d = $startDir $d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) { for ($i = 0; $i -lt 20 -and $d; $i++) {
@@ -63,10 +73,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
try { try {
$rp = $targetPath $rp = $targetPath
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {} try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
if (Test-ExternalObjectRoot $rp) { return }
$elemUuid = Get-RootUuid $rp $elemUuid = Get-RootUuid $rp
$cfgDir = $null; $binPath = $null $cfgDir = $null; $binPath = $null
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) } $d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
for ($i = 0; $i -lt 12 -and $d; $i++) { for ($i = 0; $i -lt 12 -and $d; $i++) {
if (Test-ExternalObjectRoot "$d.xml") { return }
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" } if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $cfgDir) { if (-not $cfgDir) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
+18 -1
View File
@@ -1,4 +1,4 @@
# form-edit v1.3 — Edit 1C managed form elements (Python port) # form-edit v1.4 — Edit 1C managed form elements (Python port)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
import json import json
@@ -31,6 +31,18 @@ def _sg_root_uuid(xml_path):
return None return None
def _sg_is_external_root(xml_path):
if not os.path.isfile(xml_path):
return False
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str):
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
except Exception:
return False
return False
def _sg_find_v8project(start_dir): def _sg_find_v8project(start_dir):
d = start_dir d = start_dir
for _ in range(20): for _ in range(20):
@@ -70,6 +82,9 @@ def _sg_get_edit_mode(cfg_dir):
def assert_edit_allowed(target_path, require): def assert_edit_allowed(target_path, require):
try: try:
rp = os.path.abspath(target_path) rp = os.path.abspath(target_path)
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
if _sg_is_external_root(rp):
return
elem_uuid = _sg_root_uuid(rp) elem_uuid = _sg_root_uuid(rp)
cfg_dir = None cfg_dir = None
bin_path = None bin_path = None
@@ -77,6 +92,8 @@ def assert_edit_allowed(target_path, require):
for _ in range(12): for _ in range(12):
if not d: if not d:
break break
if _sg_is_external_root(d + ".xml"):
return
if not elem_uuid: if not elem_uuid:
elem_uuid = _sg_root_uuid(d + ".xml") elem_uuid = _sg_root_uuid(d + ".xml")
if not cfg_dir: if not cfg_dir:
+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>"
``` ```
## Параметры ## Параметры
+15 -2
View File
@@ -1,4 +1,4 @@
# form-info v1.4 — Analyze 1C managed form structure # form-info v1.5 — Analyze 1C managed form structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory=$true)] [Parameter(Mandatory=$true)]
@@ -372,6 +372,16 @@ if ($formsIdx -ge 0 -and ($formsIdx + 1) -lt $parts.Count) {
# See docs/1c-support-state-spec.md. Walks up from the target path, taking the # See docs/1c-support-state-spec.md. Walks up from the target path, taking the
# uuid of the nearest element meta-xml (form/template/etc.) and the config root # uuid of the nearest element meta-xml (form/template/etc.) and the config root
# bin. Never throws — degrades to "не на поддержке". # bin. Never throws — degrades to "не на поддержке".
function Test-ExternalObjectRoot([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $false }
try {
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
$el = $mx.DocumentElement.FirstChild
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
} catch {}
return $false
}
function Get-SupportStatusForPath([string]$targetPath) { function Get-SupportStatusForPath([string]$targetPath) {
try { try {
$rp = (Resolve-Path $targetPath).Path $rp = (Resolve-Path $targetPath).Path
@@ -390,8 +400,10 @@ function Get-SupportStatusForPath([string]$targetPath) {
} }
# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml). # The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
$elemUuid = Get-RootUuid $rp $elemUuid = Get-RootUuid $rp
if (Test-ExternalObjectRoot $rp) { return $null }
$d = [System.IO.Path]::GetDirectoryName($rp) $d = [System.IO.Path]::GetDirectoryName($rp)
for ($i = 0; $i -lt 12 -and $d; $i++) { for ($i = 0; $i -lt 12 -and $d; $i++) {
if (Test-ExternalObjectRoot "$d.xml") { return $null }
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" } if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $binPath) { if (-not $binPath) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
@@ -448,7 +460,8 @@ if ($formTitle) { $header += " — `"$formTitle`"" }
if ($objectContext) { $header += " ($objectContext)" } if ($objectContext) { $header += " ($objectContext)" }
$header += " ===" $header += " ==="
$lines += $header $lines += $header
$lines += "Поддержка: $(Get-SupportStatusForPath $FormPath)" $support = Get-SupportStatusForPath $FormPath
if ($null -ne $support) { $lines += "Поддержка: $support" }
# --- Form properties (Title excluded — shown in header) --- # --- Form properties (Title excluded — shown in header) ---
+19 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# form-info v1.4 — Analyze 1C managed form structure # form-info v1.5 — Analyze 1C managed form structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -353,14 +353,29 @@ def get_support_status_for_path(target_path):
except Exception: except Exception:
pass pass
return None return None
def is_external_root(xml_path):
if not os.path.isfile(xml_path):
return False
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str):
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
except Exception:
return False
return False
rp = os.path.abspath(target_path) rp = os.path.abspath(target_path)
# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml). # The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
elem_uuid = root_uuid(rp) elem_uuid = root_uuid(rp)
if is_external_root(rp):
return None
bin_path = None bin_path = None
d = os.path.dirname(rp) d = os.path.dirname(rp)
for _ in range(12): for _ in range(12):
if not d: if not d:
break break
if is_external_root(d + ".xml"):
return None
if not elem_uuid: if not elem_uuid:
elem_uuid = root_uuid(d + ".xml") elem_uuid = root_uuid(d + ".xml")
if not bin_path: if not bin_path:
@@ -513,7 +528,9 @@ def main():
header += f" ({object_context})" header += f" ({object_context})"
header += " ===" header += " ==="
lines.append(header) lines.append(header)
lines.append(f"Поддержка: {get_support_status_for_path(form_path)}") _support = get_support_status_for_path(form_path)
if _support is not None:
lines.append(f"Поддержка: {_support}")
# --- Form properties (Title excluded -- shown in header) --- # --- Form properties (Title excluded -- shown in header) ---
prop_names = [ prop_names = [
+1 -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>"]
``` ```
## Что удаляется ## Что удаляется
@@ -1,4 +1,4 @@
# form-remove v1.2 — Remove form from 1C object # form-remove v1.3 — Remove form from 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -68,10 +68,14 @@ foreach ($node in $formNodes) {
} }
} }
# Очистить DefaultForm если указывала на эту форму # Очистить любые Default*/Auxiliary* form-слоты, указывавшие на удалённую форму
$defaultForm = $xmlDoc.SelectSingleNode("//md:DefaultForm", $nsMgr) # (form-add пишет свойство по назначению: DefaultObjectForm/DefaultListForm/
if ($defaultForm -and $defaultForm.InnerText -match "Form\.$FormName$") { # DefaultChoiceForm/DefaultRecordForm/DefaultForm — не только generic DefaultForm).
$defaultForm.InnerText = "" $formRefRe = "Form\.$([regex]::Escape($FormName))$"
foreach ($node in $xmlDoc.SelectNodes("//md:*", $nsMgr)) {
if ($node.LocalName -like "*Form" -and $node.InnerText -and $node.InnerText -match $formRefRe) {
$node.InnerText = ""
}
} }
# Сохранить с BOM # Сохранить с BOM
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# remove-form v1.1 — Remove form from 1C object # remove-form v1.3 — Remove form from 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -85,11 +85,15 @@ def main():
parent.remove(node) parent.remove(node)
break break
# Clear DefaultForm if it pointed to removed form # Clear any Default*/Auxiliary* form slot that pointed to the removed form
default_form = root.find(".//md:DefaultForm", NSMAP) # (form-add writes the purpose-specific property: DefaultObjectForm / DefaultListForm /
if default_form is not None and default_form.text: # DefaultChoiceForm / DefaultRecordForm / DefaultForm — not just generic DefaultForm).
if re.search(rf"Form\.{re.escape(form_name)}$", default_form.text): ref_re = re.compile(rf"Form\.{re.escape(form_name)}$")
default_form.text = "" for el in root.iter():
if not isinstance(el.tag, str):
continue
if etree.QName(el).localname.endswith("Form") and el.text and ref_re.search(el.text):
el.text = ""
# Save with BOM # Save with BOM
save_xml_with_bom(tree, root_xml_full) save_xml_with_bom(tree, root_xml_full)
+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.7 — Validate 1C managed form # form-validate v1.8 — 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)]
@@ -382,6 +382,10 @@ if (-not $stopped) {
$pathChecked = 0 $pathChecked = 0
$pathBaseSkipped = 0 $pathBaseSkipped = 0
# All data-binding tags whose value is an attribute path (root must exist in <Attributes>).
$bindingTags = @('DataPath','TitleDataPath','FooterDataPath','HeaderDataPath',
'MultipleValueDataPath','MultipleValuePresentDataPath','RowPictureDataPath','MultipleValuePictureDataPath')
foreach ($el in $allElements) { foreach ($el in $allElements) {
if ($stopped) { break } if ($stopped) { break }
$tag = $el.Tag $tag = $el.Tag
@@ -398,60 +402,63 @@ if (-not $stopped) {
try { if ([int]$el.Id -lt 1000000) { $pathBaseSkipped++; continue } } catch {} try { if ([int]$el.Id -lt 1000000) { $pathBaseSkipped++; continue } } catch {}
} }
$dpNode = $node.SelectSingleNode("f:DataPath", $nsMgr) foreach ($bTag in $bindingTags) {
if (-not $dpNode) { continue } if ($stopped) { break }
$dpNode = $node.SelectSingleNode("f:$bTag", $nsMgr)
if (-not $dpNode) { continue }
$dataPath = $dpNode.InnerText.Trim() $dataPath = $dpNode.InnerText.Trim()
if (-not $dataPath) { continue } if (-not $dataPath) { continue }
# Opaque platform-internal DataPath shapes — not validatable from Form.xml alone: # Opaque platform-internal shapes — not validatable from Form.xml alone:
# - bare numeric (e.g. "10", "1000003") — internal index # - bare numeric (e.g. "10", "1000003") — internal index
# - "N/M:<uuid>" — metadata reference by UUID # - "N/M:<uuid>" — metadata reference by UUID
if ($dataPath -match '^\d+$' -or $dataPath -match '^\d+/\d+:[0-9a-fA-F-]+$') { if ($dataPath -match '^\d+$' -or $dataPath -match '^\d+/\d+:[0-9a-fA-F-]+$') {
continue
}
$pathChecked++
# Extract root segment of path, strip array indices like [0]
$cleanPath = $dataPath -replace '\[\d+\]', ''
# Strip leading '~' (current row of DynamicList: ~Список.Поле)
if ($cleanPath.StartsWith('~')) { $cleanPath = $cleanPath.Substring(1) }
$segments = $cleanPath -split '\.'
$rootAttr = $segments[0]
# Resolve Items.<TableName>.CurrentData.<Field>... — table element, not attribute
if ($rootAttr -eq 'Items') {
if ($segments.Count -lt 3 -or $segments[2] -ne 'CurrentData') {
Report-Warn "[$tag] '$elName': DataPath='$dataPath' — unknown Items.* shape, expected Items.<Table>.CurrentData.*"
continue continue
} }
$tableName = $segments[1]
$tableEl = $null $pathChecked++
foreach ($candidate in $allElements) {
if ($candidate.Tag -eq 'Table' -and $candidate.Name -eq $tableName) { # Extract root segment of path, strip array indices like [0]
$tableEl = $candidate $cleanPath = $dataPath -replace '\[\d+\]', ''
break # Strip leading '~' (current row of DynamicList: ~Список.Поле)
if ($cleanPath.StartsWith('~')) { $cleanPath = $cleanPath.Substring(1) }
$segments = $cleanPath -split '\.'
$rootAttr = $segments[0]
# Resolve Items.<TableName>.CurrentData.<Field>... — table element, not attribute
if ($rootAttr -eq 'Items') {
if ($segments.Count -lt 3 -or $segments[2] -ne 'CurrentData') {
Report-Warn "[$tag] '$elName': $bTag='$dataPath' — unknown Items.* shape, expected Items.<Table>.CurrentData.*"
continue
} }
$tableName = $segments[1]
$tableEl = $null
foreach ($candidate in $allElements) {
if ($candidate.Tag -eq 'Table' -and $candidate.Name -eq $tableName) {
$tableEl = $candidate
break
}
}
if (-not $tableEl) {
Report-Error "[$tag] '$elName': $bTag='$dataPath' — table element '$tableName' not found"
$pathErrors++
continue
}
$tableDpNode = $tableEl.Node.SelectSingleNode("f:DataPath", $nsMgr)
if (-not $tableDpNode -or -not $tableDpNode.InnerText.Trim()) {
# Table without DataPath — can't resolve further, accept silently
continue
}
$tableDp = $tableDpNode.InnerText.Trim() -replace '\[\d+\]', ''
if ($tableDp.StartsWith('~')) { $tableDp = $tableDp.Substring(1) }
$rootAttr = ($tableDp -split '\.')[0]
} }
if (-not $tableEl) {
Report-Error "[$tag] '$elName': DataPath='$dataPath' — table element '$tableName' not found"
$pathErrors++
continue
}
$tableDpNode = $tableEl.Node.SelectSingleNode("f:DataPath", $nsMgr)
if (-not $tableDpNode -or -not $tableDpNode.InnerText.Trim()) {
# Table without DataPath — can't resolve further, accept silently
continue
}
$tableDp = $tableDpNode.InnerText.Trim() -replace '\[\d+\]', ''
if ($tableDp.StartsWith('~')) { $tableDp = $tableDp.Substring(1) }
$rootAttr = ($tableDp -split '\.')[0]
}
if (-not $attrMap.ContainsKey($rootAttr)) { if (-not $attrMap.ContainsKey($rootAttr)) {
Report-Error "[$tag] '$elName': DataPath='$dataPath' — attribute '$rootAttr' not found" Report-Error "[$tag] '$elName': $bTag='$dataPath' — attribute '$rootAttr' not found"
$pathErrors++ $pathErrors++
}
} }
} }
@@ -462,9 +469,9 @@ if (-not $stopped) {
$pathMsg = if ($pathMsg) { "$pathMsg, $skipNote" } else { $skipNote } $pathMsg = if ($pathMsg) { "$pathMsg, $skipNote" } else { $skipNote }
} }
if ($pathErrors -eq 0 -and $pathMsg) { if ($pathErrors -eq 0 -and $pathMsg) {
Report-OK "DataPath references: $pathMsg" Report-OK "Data bindings: $pathMsg"
} elseif ($pathErrors -eq 0) { } elseif ($pathErrors -eq 0) {
Report-OK "DataPath references: none" Report-OK "Data bindings: none"
} }
} }
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# form-validate v1.7 — Validate 1C managed form # form-validate v1.8 — 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
@@ -379,6 +379,10 @@ def main():
path_checked = 0 path_checked = 0
path_base_skipped = 0 path_base_skipped = 0
# All data-binding tags whose value is an attribute path (root must exist in <Attributes>).
binding_tags = ["DataPath", "TitleDataPath", "FooterDataPath", "HeaderDataPath",
"MultipleValueDataPath", "MultipleValuePresentDataPath", "RowPictureDataPath", "MultipleValuePictureDataPath"]
skip_tags = {"ContextMenu", "ExtendedTooltip", "AutoCommandBar", "SearchStringAddition", "ViewStatusAddition", "SearchControlAddition"} skip_tags = {"ContextMenu", "ExtendedTooltip", "AutoCommandBar", "SearchStringAddition", "ViewStatusAddition", "SearchControlAddition"}
for el in all_elements: for el in all_elements:
@@ -399,55 +403,58 @@ def main():
except (ValueError, TypeError): except (ValueError, TypeError):
pass pass
dp_node = node.find(f"{{{F_NS}}}DataPath") for b_tag in binding_tags:
if dp_node is None: if stopped:
continue break
dp_node = node.find(f"{{{F_NS}}}{b_tag}")
data_path = (dp_node.text or "").strip() if dp_node is None:
if not data_path:
continue
# Opaque platform-internal DataPath shapes — not validatable from Form.xml alone:
# - bare numeric (e.g. "10", "1000003") — internal index
# - "N/M:<uuid>" — metadata reference by UUID
if re.match(r'^\d+$', data_path) or re.match(r'^\d+/\d+:[0-9a-fA-F-]+$', data_path):
continue
path_checked += 1
clean_path = re.sub(r'\[\d+\]', '', data_path)
# Strip leading '~' (current row of DynamicList: ~\u0421\u043f\u0438\u0441\u043e\u043a.\u041f\u043e\u043b\u0435)
if clean_path.startswith('~'):
clean_path = clean_path[1:]
segments = clean_path.split(".")
root_attr = segments[0]
# Resolve Items.<TableName>.CurrentData.<Field>... \u2014 table element, not attribute
if root_attr == 'Items':
if len(segments) < 3 or segments[2] != 'CurrentData':
report_warn(f"[{tag}] '{el_name}': DataPath='{data_path}' \u2014 unknown Items.* shape, expected Items.<Table>.CurrentData.*")
continue continue
table_name = segments[1]
table_el = None data_path = (dp_node.text or "").strip()
for candidate in all_elements: if not data_path:
if candidate["Tag"] == 'Table' and candidate["Name"] == table_name: continue
table_el = candidate
break # Opaque platform-internal shapes — not validatable from Form.xml alone:
if table_el is None: # - bare numeric (e.g. "10", "1000003") — internal index
report_error(f"[{tag}] '{el_name}': DataPath='{data_path}' \u2014 table element '{table_name}' not found") # - "N/M:<uuid>" — metadata reference by UUID
if re.match(r'^\d+$', data_path) or re.match(r'^\d+/\d+:[0-9a-fA-F-]+$', data_path):
continue
path_checked += 1
clean_path = re.sub(r'\[\d+\]', '', data_path)
# Strip leading '~' (current row of DynamicList: ~Список.Поле)
if clean_path.startswith('~'):
clean_path = clean_path[1:]
segments = clean_path.split(".")
root_attr = segments[0]
# Resolve Items.<TableName>.CurrentData.<Field>... — table element, not attribute
if root_attr == 'Items':
if len(segments) < 3 or segments[2] != 'CurrentData':
report_warn(f"[{tag}] '{el_name}': {b_tag}='{data_path}' — unknown Items.* shape, expected Items.<Table>.CurrentData.*")
continue
table_name = segments[1]
table_el = None
for candidate in all_elements:
if candidate["Tag"] == 'Table' and candidate["Name"] == table_name:
table_el = candidate
break
if table_el is None:
report_error(f"[{tag}] '{el_name}': {b_tag}='{data_path}' — table element '{table_name}' not found")
path_errors += 1
continue
table_dp_node = table_el["Node"].find(f"{{{F_NS}}}DataPath")
if table_dp_node is None or not (table_dp_node.text or "").strip():
continue
table_dp = re.sub(r'\[\d+\]', '', (table_dp_node.text or "").strip())
if table_dp.startswith('~'):
table_dp = table_dp[1:]
root_attr = table_dp.split(".")[0]
if root_attr not in attr_map:
report_error(f"[{tag}] '{el_name}': {b_tag}='{data_path}' — attribute '{root_attr}' not found")
path_errors += 1 path_errors += 1
continue
table_dp_node = table_el["Node"].find(f"{{{F_NS}}}DataPath")
if table_dp_node is None or not (table_dp_node.text or "").strip():
continue
table_dp = re.sub(r'\[\d+\]', '', (table_dp_node.text or "").strip())
if table_dp.startswith('~'):
table_dp = table_dp[1:]
root_attr = table_dp.split(".")[0]
if root_attr not in attr_map:
report_error(f"[{tag}] '{el_name}': DataPath='{data_path}' \u2014 attribute '{root_attr}' not found")
path_errors += 1
path_msg = "" path_msg = ""
if path_checked > 0: if path_checked > 0:
@@ -456,7 +463,7 @@ def main():
skip_note = f"{path_base_skipped} base skipped" skip_note = f"{path_base_skipped} base skipped"
path_msg = f"{path_msg}, {skip_note}" if path_msg else skip_note path_msg = f"{path_msg}, {skip_note}" if path_msg else skip_note
if path_errors == 0 and path_msg: if path_errors == 0 and path_msg:
report_ok(f"DataPath references: {path_msg}") report_ok(f"Data bindings: {path_msg}")
# --- Check 6: Button command references --- # --- Check 6: Button command references ---
if not stopped: if not stopped:
@@ -724,7 +731,7 @@ def main():
# ExternalDataProcessorObject/ExternalReportObject valid only in EPF/ERF context # ExternalDataProcessorObject/ExternalReportObject valid only in EPF/ERF context
if is_config_context and prefix in ('ExternalDataProcessorObject', 'ExternalReportObject'): if is_config_context and prefix in ('ExternalDataProcessorObject', 'ExternalReportObject'):
report_error(f'12. Type "{tv}": External* type in configuration context (use DataProcessorObject/ReportObject instead)') report_error(f'12. Type "{tv}": External* type in configuration context (use DataProcessorObject/ReportObject instead)')
type_invalid += 1 type_error_count += 1
else: else:
report_warn(f'12. Type "{tv}": unrecognized cfg prefix') report_warn(f'12. Type "{tv}": unrecognized cfg prefix')
type_warn_count += 1 type_warn_count += 1
+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>"]
``` ```
## Что делает скрипт ## Что делает скрипт
+14 -1
View File
@@ -1,4 +1,4 @@
# help-add v1.7 — Add built-in help to 1C object # help-add v1.8 — Add built-in help to 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -28,6 +28,16 @@ function Get-RootUuid([string]$xmlPath) {
} catch {} } catch {}
return $null return $null
} }
function Test-ExternalObjectRoot([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $false }
try {
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
$el = $mx.DocumentElement.FirstChild
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
} catch {}
return $false
}
function Find-V8Project([string]$startDir) { function Find-V8Project([string]$startDir) {
$d = $startDir $d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) { for ($i = 0; $i -lt 20 -and $d; $i++) {
@@ -64,10 +74,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
try { try {
$rp = $targetPath $rp = $targetPath
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {} try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
if (Test-ExternalObjectRoot $rp) { return }
$elemUuid = Get-RootUuid $rp $elemUuid = Get-RootUuid $rp
$cfgDir = $null; $binPath = $null $cfgDir = $null; $binPath = $null
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) } $d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
for ($i = 0; $i -lt 12 -and $d; $i++) { for ($i = 0; $i -lt 12 -and $d; $i++) {
if (Test-ExternalObjectRoot "$d.xml") { return }
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" } if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $cfgDir) { if (-not $cfgDir) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
+18 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# add-help v1.7 — Add built-in help to 1C object # add-help v1.8 — Add built-in help to 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -33,6 +33,18 @@ def _sg_root_uuid(xml_path):
return None return None
def _sg_is_external_root(xml_path):
if not os.path.isfile(xml_path):
return False
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str):
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
except Exception:
return False
return False
def _sg_find_v8project(start_dir): def _sg_find_v8project(start_dir):
d = start_dir d = start_dir
for _ in range(20): for _ in range(20):
@@ -72,6 +84,9 @@ def _sg_get_edit_mode(cfg_dir):
def assert_edit_allowed(target_path, require): def assert_edit_allowed(target_path, require):
try: try:
rp = os.path.abspath(target_path) rp = os.path.abspath(target_path)
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
if _sg_is_external_root(rp):
return
elem_uuid = _sg_root_uuid(rp) elem_uuid = _sg_root_uuid(rp)
cfg_dir = None cfg_dir = None
bin_path = None bin_path = None
@@ -79,6 +94,8 @@ def assert_edit_allowed(target_path, require):
for _ in range(12): for _ in range(12):
if not d: if not d:
break break
if _sg_is_external_root(d + ".xml"):
return
if not elem_uuid: if not elem_uuid:
elem_uuid = _sg_root_uuid(d + ".xml") elem_uuid = _sg_root_uuid(d + ".xml")
if not cfg_dir: if not cfg_dir:
@@ -1,3 +1,6 @@
#!/usr/bin/env python3
# img-grid v1.1 — Overlay numbered grid on image
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Overlay a numbered grid on an image to help determine column/row proportions. """Overlay a numbered grid on an image to help determine column/row proportions.
Usage: python overlay-grid.py <image> [-c COLS] [-r ROWS] [-o OUTPUT] Usage: python overlay-grid.py <image> [-c COLS] [-r ROWS] [-o OUTPUT]
@@ -29,6 +32,11 @@ def main():
parser.add_argument("-o", "--output", help="Output path (default: <name>-grid.<ext>)") parser.add_argument("-o", "--output", help="Output path (default: <name>-grid.<ext>)")
args = parser.parse_args() args = parser.parse_args()
if args.cols <= 0:
parser.error("--cols must be greater than 0")
if args.rows < 0:
parser.error("--rows must be greater than or equal to 0")
src = Image.open(args.image).convert("RGBA") src = Image.open(args.image).convert("RGBA")
sw, sh = src.size sw, sh = src.size
@@ -36,7 +44,7 @@ def main():
step_x = sw / cols step_x = sw / cols
rows = args.rows rows = args.rows
if rows == 0: if rows == 0:
rows = round(sh / step_x) rows = max(1, round(sh / step_x))
step_y = sh / rows step_y = sh / rows
# Canvas with margins for labels # Canvas with margins for labels
+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.6 — Edit 1C CommandInterface.xml # interface-edit v1.7 — Edit 1C CommandInterface.xml
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)][Alias('Path')][string]$CIPath, [Parameter(Mandatory)][Alias('Path')][string]$CIPath,
@@ -39,6 +39,16 @@ function Get-RootUuid([string]$xmlPath) {
} catch {} } catch {}
return $null return $null
} }
function Test-ExternalObjectRoot([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $false }
try {
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
$el = $mx.DocumentElement.FirstChild
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
} catch {}
return $false
}
function Find-V8Project([string]$startDir) { function Find-V8Project([string]$startDir) {
$d = $startDir $d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) { for ($i = 0; $i -lt 20 -and $d; $i++) {
@@ -75,10 +85,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
try { try {
$rp = $targetPath $rp = $targetPath
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {} try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
if (Test-ExternalObjectRoot $rp) { return }
$elemUuid = Get-RootUuid $rp $elemUuid = Get-RootUuid $rp
$cfgDir = $null; $binPath = $null $cfgDir = $null; $binPath = $null
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) } $d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
for ($i = 0; $i -lt 12 -and $d; $i++) { for ($i = 0; $i -lt 12 -and $d; $i++) {
if (Test-ExternalObjectRoot "$d.xml") { return }
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" } if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $cfgDir) { if (-not $cfgDir) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# interface-edit v1.6 — Edit 1C CommandInterface.xml # interface-edit v1.7 — Edit 1C CommandInterface.xml
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -31,6 +31,18 @@ def _sg_root_uuid(xml_path):
return None return None
def _sg_is_external_root(xml_path):
if not os.path.isfile(xml_path):
return False
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str):
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
except Exception:
return False
return False
def _sg_find_v8project(start_dir): def _sg_find_v8project(start_dir):
d = start_dir d = start_dir
for _ in range(20): for _ in range(20):
@@ -70,6 +82,9 @@ def _sg_get_edit_mode(cfg_dir):
def assert_edit_allowed(target_path, require): def assert_edit_allowed(target_path, require):
try: try:
rp = os.path.abspath(target_path) rp = os.path.abspath(target_path)
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
if _sg_is_external_root(rp):
return
elem_uuid = _sg_root_uuid(rp) elem_uuid = _sg_root_uuid(rp)
cfg_dir = None cfg_dir = None
bin_path = None bin_path = None
@@ -77,6 +92,8 @@ def assert_edit_allowed(target_path, require):
for _ in range(12): for _ in range(12):
if not d: if not d:
break break
if _sg_is_external_root(d + ".xml"):
return
if not elem_uuid: if not elem_uuid:
elem_uuid = _sg_root_uuid(d + ".xml") elem_uuid = _sg_root_uuid(d + ".xml")
if not cfg_dir: if not cfg_dir:
+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"
``` ```
+78 -62
View File
@@ -9,111 +9,127 @@ allowed-tools:
- Glob - Glob
--- ---
# /meta-compile — генерация объектов метаданных из JSON DSL # /meta-compile — генерация объектов метаданных из JSON
Принимает JSON-определение объекта метаданных → генерирует XML + модули в структуре выгрузки конфигурации + регистрирует в Configuration.xml. Принимает JSON-определение объекта → генерирует XML + модули в структуре выгрузки конфигурации и
регистрирует объект в `Configuration.xml`.
## Порядок работы ## Порядок работы
1. Составь JSON по синтаксису и примерам ниже → запиши во временный файл 1. Составь JSON по синтаксису ниже → запиши во временный файл.
2. Запусти скрипт meta-compile 2. Запусти скрипт.
3. Если нужно изменить созданный объект — `/meta-edit` 3. Изменить созданный объект — `/meta-edit`; проверить — `/meta-validate`.
4. Если нужно проверить — `/meta-validate`
## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-compile.ps1" -JsonPath "<json>" -OutputDir "<ConfigDir>" python "${CLAUDE_SKILL_DIR}/scripts/meta-compile.py" -JsonPath "<json>" -OutputDir "<ConfigDir>"
``` ```
| Параметр | Описание | | Параметр | Описание |
|----------|----------| |----------|----------|
| `JsonPath` | Путь к JSON-файлу (один объект `{...}` или массив `[{...}, ...]`) | | `JsonPath` | Путь к JSON-файлу |
| `OutputDir` | Корень выгрузки конфигурации (где `Configuration.xml`, `Catalogs/`, `Documents/` и т.д.) | | `OutputDir` | Корень выгрузки конфигурации (где `Configuration.xml`, `Catalogs/`, `Documents/`, …) |
## JSON DSL ## Формат JSON
### Общая структура **Один объект** `{ ... }` или **массив** объектов `[{ ... }, { ... }]` (batch — несколько объектов за прогон).
```json ```json
{ "type": "Catalog", "name": "Номенклатура", ...свойства типа... } { "type": "Catalog", "name": "Номенклатура", "...свойства типа...": "..." }
``` ```
`type` и `name` — обязательные. `synonym` генерируется из `name` автоматически (CamelCase → слова через пробел). Можно задать явно: `"synonym": "Мой синоним"`. `type` и `name` — обязательные. Остальное — по типу (см. индекс ниже). `synonym` по умолчанию выводится из
`name` (CamelCase → слова через пробел); можно задать явно строкой или мультиязычно: `"synonym": { "ru": "…", "en": "…" }`.
### Shorthand реквизитов ## Реквизиты (shorthand)
Используется в `attributes`, `dimensions`, `resources`, `tabularSections`: Массивы `attributes`, `dimensions`, `resources` и колонки в `tabularSections` задаются строками:
``` ```
"ИмяРеквизита" → String(10) по умолчанию "Имя" → String(10)
"ИмяРеквизита: Тип" → с типом "Имя: Тип" → с типом
"ИмяРеквизита: Тип | req, index" → с флагами "Имя: Тип | req, index" с флагами
``` ```
Типы: `String(100)`, `Number(15,2)`, `Boolean`, `Date`, `DateTime`, `CatalogRef.Xxx`, `DocumentRef.Xxx`, `EnumRef.Xxx`, `DefinedType.Xxx` и др. ссылочные. **Типы:** `String(100)`, `String(10, fixed)` (фикс. длина), `Number(15,2)`, `Boolean`, `Date`, `DateTime`,
`Time`, ссылочные `CatalogRef.Xxx` / `DocumentRef.Xxx` / `EnumRef.Xxx` / `DefinedType.Xxx` и т.п.
Составной тип — через `+`: `"Значение: String + Number(15,2) + CatalogRef.Контрагенты"`.
Составной тип: `"Значение: String + Number(15,2) + CatalogRef.Контрагенты"`. **Флаги** (после `|`, через запятую):
Флаги: `req`, `index`, `indexAdditional`, `nonneg`, `master`, `mainFilter`, `denyIncomplete`, `useInTotals`. | Флаг | Значение | Где |
|------|----------|-----|
| `req` | обязательное заполнение | attributes, dimensions, resources |
| `index` | индексировать | attributes, dimensions |
| `indexAdditional` | индекс с доп. упорядочиванием | attributes |
| `multiline` | многострочное поле | attributes |
| `nonneg` | неотрицательное (Number) | attributes, resources |
| `master` | ведущее измерение | dimensions (регистры) |
| `mainFilter` | основной отбор | dimensions (регистры) |
| `denyIncomplete` | запрет незаполненных | dimensions |
| `useInTotals` | использовать в итогах | dimensions (регистр накопления) |
### Свойства по типам Реквизиту нужны свойства сверх shorthand (значение заполнения, параметры выбора, формат, подсказка, …) —
задаётся **объектной формой**, см. `reference/attributes.md`.
Примеров и shorthand-синтаксиса выше достаточно для типовых задач. Если нужны свойства типа, не показанные в примерах, и их допустимые значения — см. reference-файл: ## Табличные части
- `reference/types-basic.md` — Catalog, Document, Enum, Constant, DefinedType, Report, DataProcessor
- `reference/types-registers.md` — InformationRegister, AccumulationRegister, AccountingRegister, CalculationRegister, ChartOfAccounts, ChartOfCharacteristicTypes, ChartOfCalculationTypes
- `reference/types-process.md` — BusinessProcess, Task, ExchangePlan, CommonModule, ScheduledJob, EventSubscription, DocumentJournal
- `reference/types-web.md` — HTTPService, WebService
Эта инструкция и reference-файлы — полная документация для генерации. Не ищи примеры XML в выгрузках конфигураций.
## Примеры паттернов DSL
### Минимальный объект
```json ```json
{ "type": "Catalog", "name": "Валюты" } "tabularSections": { "Товары": ["Номенклатура: CatalogRef.Номенклатура", "Количество: Number(15,3)"] }
``` ```
### С реквизитами Ключ — имя ТЧ, значение — массив колонок (shorthand) ЛИБО объект со свойствами ТЧ (см. `reference/attributes.md`).
## Индекс: свойства по типам
Для каждого типа — свой reference-файл со свойствами, дефолтами и допустимыми значениями:
| Тип(ы) | Файл |
|--------|------|
| Catalog (справочник) | `reference/catalog.md` |
| Document, DocumentJournal, Sequence, DocumentNumerator | `reference/document.md` |
| InformationRegister, AccumulationRegister, AccountingRegister, CalculationRegister | `reference/registers.md` |
| ChartOfAccounts, ChartOfCharacteristicTypes, ChartOfCalculationTypes | `reference/charts.md` |
| ExchangePlan | `reference/exchangeplan.md` |
| BusinessProcess, Task | `reference/process.md` |
| Report, DataProcessor | `reference/report-dataprocessor.md` |
| CommonModule, ScheduledJob, EventSubscription | `reference/code.md` |
| HTTPService, WebService | `reference/web.md` |
| Enum, Constant, DefinedType | `reference/simple.md` |
| FunctionalOption, FilterCriterion, SettingsStorage, CommonForm, CommonPicture, CommonTemplate, служебные | `reference/other-types.md` |
Кросс-типовые детали:
- **`reference/attributes.md`** — объектная форма реквизита и колонки ТЧ (значение заполнения, параметры
выбора, формат, подсказка, границы, …) + свойства самой ТЧ.
- **`reference/blocks.md`** — блоки объекта: представления, команды (+ характеристики/стандартные реквизиты).
Эта инструкция и reference-файлы — полная документация. Не ищи примеры XML в выгрузках конфигураций.
## Примеры
Справочник с реквизитами:
```json ```json
{ { "type": "Catalog", "name": "Организации", "descriptionLength": 100,
"type": "Catalog", "name": "Организации", "attributes": ["ИНН: String(12)", "КПП: String(9)", "Директор: CatalogRef.ФизическиеЛица"] }
"descriptionLength": 100,
"attributes": ["ИНН: String(12)", "КПП: String(9)", "Директор: CatalogRef.ФизическиеЛица"]
}
``` ```
### С табличной частью Документ с движениями и ТЧ:
```json ```json
{ { "type": "Document", "name": "ПриходнаяНакладная",
"type": "Document", "name": "ПриходнаяНакладная",
"registerRecords": ["AccumulationRegister.ОстаткиТоваров"], "registerRecords": ["AccumulationRegister.ОстаткиТоваров"],
"attributes": ["Организация: CatalogRef.Организации", "Контрагент: CatalogRef.Контрагенты"], "attributes": ["Организация: CatalogRef.Организации", "Контрагент: CatalogRef.Контрагенты"],
"tabularSections": { "Товары": ["Номенклатура: CatalogRef.Номенклатура", "Количество: Number(15,3)", "Цена: Number(15,2)"] } "tabularSections": { "Товары": ["Номенклатура: CatalogRef.Номенклатура", "Количество: Number(15,3)", "Цена: Number(15,2)"] } }
}
``` ```
### Регистровый паттерн (измерения + ресурсы) Регистр сведений:
```json ```json
{ { "type": "InformationRegister", "name": "КурсыВалют", "periodicity": "Day",
"type": "InformationRegister", "name": "КурсыВалют", "periodicity": "Day",
"dimensions": ["Валюта: CatalogRef.Валюты | master, mainFilter, denyIncomplete"], "dimensions": ["Валюта: CatalogRef.Валюты | master, mainFilter, denyIncomplete"],
"resources": ["Курс: Number(15,4)", "Кратность: Number(10,0)"] "resources": ["Курс: Number(15,4)", "Кратность: Number(10,0)"] }
}
``` ```
### Batch — несколько объектов в одном файле Batch:
```json ```json
[ [ { "type": "Enum", "name": "Статусы", "values": ["Новый", "Закрыт"] },
{ "type": "Enum", "name": "Статусы", "values": ["Новый", "Закрыт"] },
{ "type": "Catalog", "name": "Валюты" }, { "type": "Catalog", "name": "Валюты" },
{ "type": "Constant", "name": "ОсновнаяВалюта", "valueType": "CatalogRef.Валюты" } { "type": "Constant", "name": "ОсновнаяВалюта", "valueType": "CatalogRef.Валюты" } ]
]
``` ```
@@ -0,0 +1,139 @@
# Объектная форма реквизита и табличной части
Когда реквизиту (в `attributes` / `dimensions` / `resources` / колонках ТЧ) нужны свойства сверх
shorthand — вместо строки задаётся объект:
```json
{ "name": "Цена", "type": "Number(15,2)", "tooltip": "Цена за единицу", "fillValue": 0 }
```
`name` и `type` обязательны (тип можно задать и раздельно: `"type": "Number", "length": 15, "precision": 2`).
Остальные ключи — ниже, все со значением по умолчанию (не задавать, если устраивает дефолт).
## Свойства реквизита
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `synonym` | из имени | ML (строка или `{ru,en}`) |
| `tooltip` | пусто | ML |
| `comment` | пусто | строка |
| `fillChecking` | `DontCheck` | `DontCheck` / `ShowError` / `ShowWarning` (то же, что флаг `req`) |
| `fullTextSearch` | `Use` | `Use` / `DontUse` |
| `fillFromFillingValue` | `false` | bool |
| `fillValue` | по типу (см. ниже) | значение заполнения |
| `createOnInput` | `Auto` | `Auto` / `Use` / `DontUse` |
| `quickChoice` | `Auto` | `Auto` / `Use` / `DontUse` |
| `choiceHistoryOnInput` | `Auto` | `Auto` / `DontUse` |
| `dataHistory` | `Use` | `Use` / `DontUse` |
| `use` | `ForItem` | `ForItem` / `ForFolder` / `ForFolderAndItem` (реквизит иерархического справочника) |
| `passwordMode` | `false` | bool |
| `multiLine` | `false` | bool (то же, что флаг `multiline`) |
| `extendedEdit` | `false` | bool (расширенное редактирование — многострочный ввод) |
| `mask` | пусто | строка маски ввода |
| `format` / `editFormat` | пусто | форматная строка 1С (ML) |
| `markNegatives` | `false` | bool (выделять отрицательные, для Number) |
| `minValue` / `maxValue` | не задано | граница диапазона (см. ниже) |
| `choiceParameterLinks` | пусто | связи параметров выбора (см. ниже) |
| `choiceParameters` | пусто | параметры выбора (см. ниже) |
| `choiceForm` | пусто | ссылка на форму выбора `Тип.Объект.Form.ИмяФормы` |
| `choiceFoldersAndItems` | `Items` | `Items` / `Folders` / `FoldersAndItems` (что выбирать в иерарх. справочнике) |
Индексирование задаётся флагом `index` / `indexAdditional` в shorthand, либо в объекте — как и в строковой форме,
через `"type": "… | index"`.
### `fillValue` — значение заполнения
Пустое значение по типу компилятор подставляет сам — ключ **не задают**:
| Тип реквизита | Пустое значение |
|---------------|-----------------|
| String | пустая строка |
| Number | `0` |
| Boolean, Date, ссылочный, составной | не задано (nil) |
Ключ `fillValue` задают для **конкретного** значения — интерпретируется по типу реквизита:
- **Boolean**`true` / `false`.
- **Number** — число (`21`, `1.5`).
- **String** — строка.
- **Date** — ISO-строка `"2020-01-01T00:00:00"`.
- **Ссылочный** — путь: `"Catalog.Валюты.EmptyRef"` (пустая ссылка), `"Enum.Периодичность.EnumValue.Месяц"`
(значение перечисления), `"Catalog.СтраныМира.Россия"` (предопределённый элемент).
- **`null`** — явно «значение не задано» (nil), когда нужно перекрыть непустой дефолт типа.
- **`{ "emptyRef": true }`** — пустая ссылка для реквизита типа `DefinedType.X` (когда тип из пути не выводится).
> Пустая ссылка (`EmptyRef`) и `null` — разное: платформа хранит их отдельно.
### `minValue` / `maxValue` — границы диапазона
Число → числовая граница; строка → строковая (напр. год `"2000"`). Без ключа — граница не задана.
### `choiceParameterLinks` — связи параметров выбора
Связывают параметр выбора этого реквизита с другим реквизитом объекта. Массив строк или объектов:
```json
"choiceParameterLinks": ["Отбор.Организация=Организация", "Отбор.Договор=Договор:DontChange"]
"choiceParameterLinks": [{ "name": "Отбор.Организация", "dataPath": "Организация", "valueChange": "Clear" }]
```
- `dataPath` — реквизит **того же объекта**: имя обычного реквизита (`"Организация"`) или стандартного
(`"Владелец"`, `"Ссылка"`).
- `valueChange``Clear` (по умолчанию) / `DontChange`.
### `choiceParameters` — параметры выбора
Фиксируют параметр выбора значением. Массив строк или объектов:
```json
"choiceParameters": ["Отбор.ЭтоГруппа=false"]
"choiceParameters": [{ "name": "Отбор.Владелец", "value": "Catalog.Организации.EmptyRef" }]
```
- `value` — bool / число / строка / ссылочный путь (несёт тип) ИЛИ массив (список фиксированных значений).
- Для набора голых имён-значений добавьте `type` (тип поля-фильтра), чтобы они стали ссылками:
`{ "name": "Отбор.Тип", "type": "EnumRef.ТипыВЕТИС", "value": ["EmptyRef", "ТТН"] }`.
### Редкие ключи
`linkByType` — связь по типу (тип реквизита-Характеристики берётся из другого реквизита):
`{ "dataPath": "Свойство", "linkItem": 0 }` или строка-путь. Применяется для реквизитов-характеристик.
---
## Табличная часть — объектная форма
Значение в `tabularSections` — массив колонок ЛИБО объект со свойствами самой ТЧ:
```json
"tabularSections": {
"Товары": {
"synonym": { "ru": "Товары", "en": "Goods" },
"tooltip": "Строки заказа",
"fillChecking": "ShowError",
"attributes": ["Номенклатура: CatalogRef.Номенклатура", "Количество: Number(15,3)"]
}
}
```
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `synonym` | из имени | ML |
| `tooltip` | пусто | ML |
| `comment` | пусто | строка |
| `fillChecking` | `DontCheck` | `DontCheck` / `ShowError` / `ShowWarning` (обязательность заполнения ТЧ) |
| `use` | `ForItem` | `ForItem` / `ForFolder` / `ForFolderAndItem` (только Catalog / ChartOfCharacteristicTypes) |
| `attributes` | `[]` | колонки (shorthand или объектная форма реквизита) |
| `lineNumber` | — | кастомизация стандартного реквизита НомерСтроки (см. ниже) |
### `lineNumber` — стандартный реквизит НомерСтроки
У каждой ТЧ есть стандартный реквизит НомерСтроки. По умолчанию все его свойства типовые. Ключ `lineNumber`
на объектной форме ТЧ их переопределяет:
```json
"Строки": { "lineNumber": { "synonym": "Номер п/п", "fullTextSearch": "DontUse" }, "attributes": [...] }
```
Переопределяемые: `synonym`, `comment`, `fullTextSearch` (`Use`/`DontUse`), `tooltip`, `format`, `editFormat`,
`choiceHistoryOnInput` (`Auto`/`DontUse`).
@@ -0,0 +1,109 @@
# Блоки объекта
Кросс-типовые блоки уровня объекта (применимы к ссылочным типам — Catalog, Document, ChartOf*, ExchangePlan,
BusinessProcess, Task и др.).
## Представления
Тексты представления объекта в интерфейсе (ML — строка или `{ru,en}`, по умолчанию пусто):
| Ключ | Смысл |
|------|-------|
| `objectPresentation` | представление объекта |
| `extendedObjectPresentation` | расширенное представление объекта |
| `listPresentation` | представление списка |
| `extendedListPresentation` | расширенное представление списка |
| `explanation` | пояснение |
Набор доступных ключей зависит от типа (у списочных без формы объекта нет `objectPresentation` и т.п.).
```json
"listPresentation": "Организации", "objectPresentation": { "ru": "Организация", "en": "Company" }
```
## Команды
Команды объекта. Ключ — имя команды, значение — объект свойств (map `имя → объект` или массив `[{name, …}]`).
Для каждой команды создаётся заготовка модуля с обработчиком `ОбработкаКоманды`.
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `synonym` | из имени | ML |
| `tooltip` | пусто | ML |
| `comment` | пусто | строка |
| `group` | **обязательно** | группа размещения (см. ниже) |
| `commandParameterType` | пусто | тип параметра (напр. `CatalogRef.Номенклатура`) — **только для групп формы** |
| `parameterUseMode` | `Single` | `Single` / `Multiple` |
| `modifiesData` | `false` | bool |
| `representation` | `Auto` | вид отображения |
| `picture` | пусто | ссылка на картинку (`StdPicture.Print`, `CommonPicture.Загрузка`) |
| `shortcut` | пусто | сочетание клавиш |
```json
"commands": {
"ПечатьЭтикеток": { "synonym": "Печать этикеток", "group": "FormCommandBarImportant",
"commandParameterType": "CatalogRef.Номенклатура", "picture": "StdPicture.Print" }
}
```
**Группа (`group`) обязательна** — каждая команда размещается в группе командного интерфейса:
- **Командный интерфейс раздела** (панель навигации / панель действий; `commandParameterType` **недоступен**):
`NavigationPanelImportant` / `NavigationPanelOrdinary` / `NavigationPanelSeeAlso`,
`ActionsPanelCreate` / `ActionsPanelReports` / `ActionsPanelTools`.
- **Командный интерфейс формы** (`commandParameterType` допустим): `FormCommandBarImportant` /
`FormCommandBarCreateBasedOn`, `FormNavigationPanelImportant` / `FormNavigationPanelGoTo` / `FormNavigationPanelSeeAlso`.
- **Кастомная группа:** `CommandGroup.<Имя>` (параметр допустим).
Группа раздела вместе с `commandParameterType` → ошибка.
## `inputByString` / `dataLockFields` / `basedOn`
Списки полей/объектов уровня объекта. Поля — по имени реквизита объекта (обычного или стандартного).
- **`inputByString`** — поля быстрого ввода по строке. По умолчанию выводятся из Кода/Наименования — ключ не нужен;
задать при другом наборе/порядке, либо `[]` для отключения.
```json
"inputByString": ["Код", "Наименование", "Контрагент"]
```
- **`dataLockFields`** — поля управляемой блокировки данных (по умолчанию пусто).
```json
"dataLockFields": ["Организация", "Контрагент"]
```
- **`basedOn`** — «ввод на основании»: список ссылок на объекты метаданных (по умолчанию пусто).
```json
"basedOn": ["Catalog.Контрагенты", "Document.ЗаказПоставщику"]
```
## `standardAttributes` — кастомизация стандартных реквизитов
Стандартные реквизиты объекта (Наименование, Код, Владелец, …) переопределяются блоком
`standardAttributes` — объект `{ ИмяРеквизита: { переопределения } }`. Имена — как в 1С: `Description`, `Code`,
`Owner`, `Parent`, `DeletionMark`, `Ref` и т.д. (для Document — `Date`, `Number`, `Posted`).
Переопределяемые поля — как у обычного реквизита (`synonym`, `tooltip`, `fillChecking`, `fillValue`,
`choiceParameters`, `comment`, `mask`, `choiceForm`; полный набор — `attributes.md`).
```json
"standardAttributes": {
"Description": { "synonym": "Наименование контрагента" },
"Code": { "fillChecking": "ShowError" }
}
```
## `characteristics` — «Дополнительные реквизиты и сведения»
Привязка плана видов характеристик. Массив; каждый элемент связывает **источник типов** (где определены
характеристики) и **источник значений** (где хранятся значения).
```json
"characteristics": [{
"types": { "from": "Catalog.НаборыДопРеквизитов.ДополнительныеРеквизиты",
"key": "Свойство", "filterField": "Ссылка", "filterValue": "Справочник_Организации" },
"values": { "from": "Catalog.Организации.TabularSection.ДополнительныеРеквизиты",
"object": "Ссылка", "type": "Свойство", "value": "Значение" }
}]
```
- `from` — таблица-источник; `key`/`filterField`/`object`/`type`/`value` — поля источника (по имени реквизита).
- `filterValue` — значение фильтра типов: имя предопределённого набора (строка) или путь к элементу.
@@ -0,0 +1,71 @@
# Catalog (Справочник)
```json
{ "type": "Catalog", "name": "Организации", "descriptionLength": 100,
"attributes": ["ИНН: String(12)", "КПП: String(9)"] }
```
## Свойства
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `comment` | пусто | строка |
| `hierarchical` | `false` | bool |
| `hierarchyType` | `HierarchyFoldersAndItems` | `HierarchyFoldersAndItems` / `HierarchyOfItems` |
| `limitLevelCount` | `false` | bool (ограничивать кол-во уровней) |
| `levelCount` | `2` | число уровней (при `limitLevelCount`) |
| `foldersOnTop` | `true` | bool (группы сверху) |
| `owners` | `[]` | массив ссылок-владельцев: `["CatalogRef.Контрагенты"]` |
| `subordinationUse` | `ToItems` | `ToItems` / `ToFolders` / `ToFoldersAndItems` (кому подчинён) |
| `codeLength` | `9` | длина кода (0 — без кода) |
| `codeType` | `String` | `String` / `Number` |
| `codeAllowedLength` | `Variable` | `Variable` / `Fixed` |
| `codeSeries` | `WholeCatalog` | `WholeCatalog` / `WithinSubordination` / `WithinOwnerSubordination` |
| `autonumbering` | `true` | bool (автонумерация) |
| `checkUnique` | `false` | bool (контроль уникальности кода) |
| `descriptionLength` | `25` | длина наименования |
| `defaultPresentation` | `AsDescription` | `AsDescription` / `AsCode` |
| `quickChoice` | `true` | bool (быстрый выбор) |
| `choiceMode` | `BothWays` | `BothWays` / `QuickChoice` / `FromForm` |
| `editType` | `InDialog` | `InDialog` / `InList` / `BothWays` |
| `createOnInput` | `Use` | `Auto` / `Use` / `DontUse` |
| `choiceHistoryOnInput` | `Auto` | `Auto` / `DontUse` |
| `fullTextSearch` | `Use` | `Use` / `DontUse` |
| `fullTextSearchOnInputByString` | `DontUse` | `Use` / `DontUse` |
| `searchStringModeOnInputByString` | `Begin` | `Begin` / `AnyPart` |
| `predefinedDataUpdate` | `Auto` | `Auto` / `DontAutoUpdate` / `AutoUpdate` |
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
| `useStandardCommands` | `true` | bool |
| `includeHelpInContents` | `false` | bool |
| `attributes` | `[]` | реквизиты (shorthand / объектная форма) |
| `tabularSections` | `{}` | табличные части |
**Формы.** Ссылка на форму — `Тип.Объект.Form.ИмяФормы` (напр. `Catalog.Организации.Form.ФормаЭлемента`).
Слоты основных форм: `defaultObjectForm`, `defaultFolderForm`, `defaultListForm`, `defaultChoiceForm`,
`defaultFolderChoiceForm`; вспомогательных — те же имена с префиксом `auxiliary` (`auxiliaryObjectForm`, …).
## `predefined` — предопределённые элементы
Массив предопределённых элементов → `Ext/Predefined.xml`. Элемент — строка (плоский случай) или объект (иерархия).
**Строка:** `"(Код) Имя [Наименование]"``Имя` обязательно; `(Код)` и `[Наименование]` опциональны.
Без `[...]` наименование выводится из имени; `[]` — пустое; `[текст]` — заданное.
```json
"predefined": [
"Основной",
"(1) ДокументОПриемке [Документ о приемке]",
{ "name": "Группа1", "isFolder": true, "description": "Прочие",
"childItems": ["Факс", "(7) Скайп"] }
]
```
**Объект:** `name` (обязательно), `code`, `description` (наименование), `isFolder` (признак группы),
`childItems` (вложенные, рекурсивно). Тип кода — по свойству `codeType`.
## Дополнительно
- Свойства реквизитов и табличных частей — `attributes.md`.
- Представления (`objectPresentation`, `listPresentation`, …), команды объекта, характеристики
(«ДопРеквизиты и сведения»), кастомизация стандартных реквизитов, `inputByString` / `dataLockFields` /
`basedOn``blocks.md`.
@@ -0,0 +1,105 @@
# Планы: ChartOfCharacteristicTypes, ChartOfAccounts, ChartOfCalculationTypes
Все три — ссылочные типы (наследуют слой Catalog: коды, `standardAttributes`, `characteristics`, `inputByString`,
формы, представления — см. `catalog.md` / `attributes.md` / `blocks.md`) с предопределёнными элементами и своими
специальными свойствами.
## ChartOfCharacteristicTypes (План видов характеристик)
Хранит определения характеристик (видов). Иерархический (папки+элементы).
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `valueType` | любой примитив | тип значения характеристики (составной — строка `"A + B"` или массив `valueTypes`) |
| `characteristicExtValues` | пусто | ссылка на справочник доп. значений |
| `hierarchical` | `false` | bool |
| `foldersOnTop` | `true` | bool |
| `codeLength` | `9` | длина кода |
| `descriptionLength` | `100` | длина наименования |
| `checkUnique` | `true` | bool |
| `autonumbering` | `true` | bool |
| `codeSeries` | `WholeCharacteristicKind` | серия кодов |
| `createOnInput` | `DontUse` | `Auto` / `Use` / `DontUse` |
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
| `predefined` | `[]` | предопределённые виды (несут тип значения — см. ниже) |
| `attributes` / `tabularSections` | `[]` / `{}` | реквизиты / табличные части |
**Предопределённые виды** несут **тип значения на элемент** — короткой строкой после `:`
(`"(Код) Имя [Наименование]: Тип"`, составной через `+`) или объектной формой с ключом `type`:
```json
"predefined": [
"(000001) Цвет: CatalogRef.Цвета",
"(000002) Размер [Размер одежды]: String(50) + Number(3,0)",
{ "name": "Группа", "isFolder": true, "type": "" }
]
```
## ChartOfAccounts (План счетов)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `extDimensionTypes` | пусто | ссылка на ПВХ видов субконто `ChartOfCharacteristicTypes.X` |
| `maxExtDimensionCount` | `0` (без ПВХ) / `3` (с ПВХ) | макс. число субконто |
| `codeMask` | пусто | маска кода счёта (напр. `"@@@.@@"`) |
| `codeLength` | `9` | длина кода |
| `descriptionLength` | `25` | длина наименования |
| `checkUnique` | `true` | bool |
| `codeSeries` | `WholeChartOfAccounts` | серия кодов |
| `defaultPresentation` | `AsCode` | `AsCode` / `AsDescription` |
| `autoOrderByCode` | `true` | bool |
| `orderLength` | `9` | длина строки упорядочивания |
| `createOnInput` | `DontUse` | `Auto` / `Use` / `DontUse` |
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
| `accountingFlags` | `[]` | признаки учёта (как реквизиты, тип по умолчанию Boolean; массив имён/реквизитов) |
| `extDimensionAccountingFlags` | `[]` | признаки учёта субконто (как реквизиты) |
| `predefined` | `[]` | предопределённые счета (см. ниже) |
**Предопределённый счёт** (объектная форма):
| Поле | Умолчание | Значения |
|------|-----------|----------|
| `name` | — | имя (обязательно) |
| `code` | пусто | код счёта |
| `description` | из имени | наименование |
| `accountType` | `ActivePassive` | `Active` / `Passive` / `ActivePassive` |
| `offBalance` | `false` | bool (забалансовый) |
| `order` | — | строка сортировки |
| `flags` | `[]` | включённые признаки учёта (только TRUE) |
| `subconto` | `[]` | виды субконто (см. ниже) |
| `childItems` | `[]` | подчинённые счета |
`subconto` — строка `"Вид | Признак1, Признак2"` (после `|` — включённые признаки учёта субконто; токен `Turnover`
«только обороты») или объект `{ type, turnover, flags }`. `Вид` — имя предопределённого вида из ПВХ `extDimensionTypes`.
```json
"predefined": [
{ "name": "ОсновныеСредства", "code": "01", "accountType": "Active", "order": " 01",
"flags": ["Количественный"], "subconto": ["Номенклатура | Суммовой, Валютный"],
"childItems": [ { "name": "ОСВОрганизации", "code": "01.01", "accountType": "Active", "order": " 01.01" } ] }
]
```
## ChartOfCalculationTypes (План видов расчёта)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `codeLength` | `5` | длина кода |
| `descriptionLength` | `100` | длина наименования |
| `codeAllowedLength` | `Variable` | `Variable` / `Fixed` |
| `dependenceOnCalculationTypes` | `DontUse` | `DontUse` / `OnPeriod` / `OnActionPeriod` |
| `baseCalculationTypes` | `[]` | базовые виды расчёта (список ссылок `ChartOfCalculationTypes.X`) |
| `actionPeriodUse` | `false` | bool (использовать период действия) |
| `createOnInput` | `DontUse` | `Auto` / `Use` / `DontUse` |
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
| `predefined` | `[]` | предопределённые виды расчёта (см. ниже) |
**Предопределённый вид расчёта** — плоский: строка `"(Код) Имя [Наименование]"` или объект
`{ name, code, description, actionPeriodIsBase }` (`actionPeriodIsBase` — bool, по умолчанию `false`).
```json
"predefined": [ "(00001) Оклад [Оклад по дням]", { "name": "Премия", "code": "00002", "actionPeriodIsBase": true } ]
```
> **ChartOfAccounts** ссылается на ПВХ через `extDimensionTypes`. Регистр бухгалтерии/расчёта требует
> соответствующий план (см. `registers.md`).
@@ -0,0 +1,56 @@
# CommonModule, ScheduledJob, EventSubscription (объекты, привязанные к коду)
## CommonModule (Общий модуль)
Флаги контекста выполнения (все bool, по умолчанию `false`). Создаёт пустой `Ext/Module.bsl`.
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `context` | — | шорткат флагов (см. ниже) |
| `global` | `false` | bool |
| `server` | `false` | bool |
| `serverCall` | `false` | bool (вызов сервера) |
| `clientManagedApplication` | `false` | bool (клиент управляемого приложения) |
| `clientOrdinaryApplication` | `false` | bool (клиент обычного приложения) |
| `externalConnection` | `false` | bool |
| `privileged` | `false` | bool |
| `returnValuesReuse` | `DontUse` | `DontUse` / `DuringRequest` / `DuringSession` |
Шорткат `context`: `"server"` → Server+ServerCall; `"client"` → ClientManagedApplication;
`"serverClient"` → Server+ClientManagedApplication.
```json
{ "type": "CommonModule", "name": "ОбменДаннымиСервер", "context": "server", "returnValuesReuse": "DuringRequest" }
```
## ScheduledJob (Регламентное задание)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `methodName` | пусто | метод-обработчик `"МодульСервер.Процедура"` (дополняется до `CommonModule.…`) |
| `description` | пусто | наименование задания |
| `key` | пусто | ключ |
| `use` | `false` | bool (использование) |
| `predefined` | `false` | bool (предопределённое) |
| `restartCountOnFailure` | `3` | число повторов при сбое |
| `restartIntervalOnFailure` | `10` | интервал повтора, сек |
```json
{ "type": "ScheduledJob", "name": "ОбменДанными", "methodName": "ОбменДаннымиСервер.Выполнить", "use": true }
```
## EventSubscription (Подписка на событие)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `source` | `[]` | объекты-источники: `["CatalogObject.Контрагенты", "DocumentObject.Реализация"]` |
| `event` | `BeforeWrite` | `BeforeWrite` / `OnWrite` / `BeforeDelete` / `OnReadAtServer` / `FillCheckProcessing` … |
| `handler` | пусто | метод-обработчик `"МодульСервер.Процедура"` (дополняется до `CommonModule.…`) |
```json
{ "type": "EventSubscription", "name": "ПередЗаписьюКонтрагента",
"source": ["CatalogObject.Контрагенты"], "event": "BeforeWrite",
"handler": "ОбщегоНазначенияСервер.ПередЗаписьюКонтрагента" }
```
> Процедура-обработчик (`methodName` / `handler`) должна существовать в указанном общем модуле (экспортная).
@@ -0,0 +1,79 @@
# Document, DocumentJournal, Sequence, DocumentNumerator
## Document (Документ)
```json
{ "type": "Document", "name": "ПриходнаяНакладная",
"registerRecords": ["AccumulationRegister.ОстаткиТоваров"],
"attributes": ["Организация: CatalogRef.Организации"],
"tabularSections": { "Товары": ["Номенклатура: CatalogRef.Номенклатура", "Количество: Number(15,3)"] } }
```
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `comment` | пусто | строка |
| `numerator` | пусто | ссылка на нумератор `DocumentNumerator.X` |
| `numberType` | `String` | `String` / `Number` |
| `numberLength` | `11` | длина номера |
| `numberAllowedLength` | `Variable` | `Variable` / `Fixed` |
| `numberPeriodicity` | `Year` | `Nonperiodical` / `Day` / `Month` / `Quarter` / `Year` |
| `checkUnique` | `true` | bool |
| `autonumbering` | `true` | bool |
| `posting` | `Allow` | `Allow` / `Deny` (проведение) |
| `realTimePosting` | `Deny` | `Allow` / `Deny` (оперативное проведение) |
| `registerRecordsDeletion` | `AutoDelete` | `AutoDelete` / `AutoDeleteOnUnpost` / `AutoDeleteOff` |
| `registerRecordsWritingOnPost` | `WriteSelected` | `WriteModified` / `WriteSelected` / `WriteAll` |
| `sequenceFilling` | `AutoFill` | заполнение последовательностей |
| `postInPrivilegedMode` | `true` | bool |
| `unpostInPrivilegedMode` | `true` | bool |
| `createOnInput` | `Use` | `Auto` / `Use` / `DontUse` |
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
| `fullTextSearch` | `Use` | `Use` / `DontUse` |
| `dataHistory` | `DontUse` | `Use` / `DontUse` |
| `registerRecords` | `[]` | движения: список ссылок `["AccumulationRegister.ОстаткиТоваров", "InformationRegister.Цены"]` |
| `useStandardCommands` | `true` | bool |
| `includeHelpInContents` | `false` | bool |
| `attributes` / `tabularSections` | `[]` / `{}` | реквизиты / табличные части |
Формы: `defaultObjectForm`, `defaultListForm`, `defaultChoiceForm`, `auxiliary*` (см. `catalog.md`).
Реквизиты и ТЧ — `attributes.md`. Представления, команды, характеристики, `basedOn`, `standardAttributes`,
`inputByString`, `dataLockFields``blocks.md`.
## DocumentJournal (Журнал документов)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `registeredDocuments` | `[]` | документы журнала: `["Document.Встреча", "Document.Звонок"]` |
| `columns` | `[]` | графы журнала (см. ниже) |
Графа — строка `"Имя"` или объект `{ name, synonym, indexing, references }`, где `indexing``Index`/`DontIndex`,
`references` — пути к реквизитам документов, отображаемым в графе.
```json
{ "type": "DocumentJournal", "name": "Взаимодействия",
"registeredDocuments": ["Document.Встреча", "Document.Звонок"],
"columns": [{ "name": "Организация", "indexing": "Index",
"references": ["Document.Встреча.Attribute.Организация"] }] }
```
## Sequence (Последовательность документов)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `moveBoundaryOnPosting` | `DontMove` | сдвиг границы при проведении |
| `documents` | `[]` | документы последовательности (список ссылок) |
| `registerRecords` | `[]` | движения (список ссылок) |
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
| `dimensions` | `[]` | измерения `{name, type, documentMap[], registerRecordsMap[]}` |
`documentMap` / `registerRecordsMap` — пути к реквизитам документов / движениям, соответствующим измерению.
## DocumentNumerator (Нумератор документов)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `numberType` | `String` | `String` / `Number` |
| `numberLength` | `11` | длина номера |
| `numberAllowedLength` | `Variable` | `Variable` / `Fixed` |
| `numberPeriodicity` | `Year` | `Nonperiodical` / `Day` / … / `Year` |
| `checkUnique` | `true` | bool |

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